乙Py先生のプログラミング教室
初学者のためのプログラミング学習サイト

自然言語処理

本日は自然言語処理100本ノックの
第二章UNIXコマンドの基礎をやっていきます。

自然言語処理100本ノック

解説動画はこちら












さて第二章はUNIXコマンドを用いて
やりなさいというのが
問題文に書いてありますので
覚えたほうが良さそうなLINUXコマンドを
挙げておきます。

覚えた方が良さそうなLinuxコマンド
cat (ファイルの内容を表示)
wc (文字や行数のカウント)
tr (文字の置換)
cut (ファイルの行から一部を切り離す)
paste (ファイルを結合する)
head (ファイルの先頭から読む)
tail (ファイルの末尾から読む)
sort (ファイルを並べ替えする)
uniq (重複行を削除する)
fold (テキストを指定した幅で改行して出力する)

こんだけ使いました。
Python言語でやるのも良いのですが
実はUNIX系のコマンドは
自然言語処理では非常に優秀なので
覚えておくと作業が捗ると思います。

この動画はMacOSでやっているので
JupyterNotebook上でもコマンドが使えますが
古いWindowsを用いている人はコマンドが使えないので
コマンドが使える環境で試して下さい。

Google Colab などであれば使えるので
アカウントを持っておくと良いかもしれません。

では問題を解いていきましょう。

第二章ではテキストファイルを用いていきます。
手元にダウンロードしておきましょう。

hightemp.txt

全問共通でこのファイルを使うので
ファイルのパスを指定しておきましょう。

file_path = 'hightemp.txt'



10. 行数のカウント

行数をカウントせよ.
確認にはwcコマンドを用いよ.

最初は行数を数える問題です。
ここら辺は基本ですね。

pythonでの行数カウントは
こんな感じですぐできます。

print(sum(1 for _ in open(file_path)))
24

Linuxコマンドの場合は

wc -l hightemp.txt
24

wcコマンドで行数を数えることができます。



11. タブをスペースに置換

タブ1文字につきスペース1文字に置換せよ.
確認にはsedコマンド,trコマンド,
もしくはexpandコマンドを用いよ.

文字列置換の問題ですね
Pythonでは文字列型の関数であるreplaceで
実現できます。
with open(file_path) as _f:
    for row in _f:
        print(row.replace('\t',' '))
高知県 江川崎 41 2013-08-12

埼玉県 熊谷 40.9 2007-08-16

・・・

Linuxコマンドは1行ですね
cat hightemp.txt | tr $'\t' ' '
高知県 江川崎 41 2013-08-12
埼玉県 熊谷 40.9 2007-08-16

catコマンドでファイル読み込んで
出力した結果を | で繋げて
trコマンドで結果を置換できます。



12. 1列目をcol1.txtに,2列目をcol2.txtに保存

各行の1列目だけを抜き出したものをcol1.txtに,
2列目だけを抜き出したものをcol2.txtとして
ファイルに保存せよ. 確認にはcutコマンドを用いよ.

ファイルに分割する問題ですね

こういった形式のファイルを読み込みする場合
楽な方法としては
Pandasのデータフレームというのが使えます。
import pandas as pd
df = pd.read_table(file_path,header=None)
df.head()
0123
0高知県江川崎41.02013-08-12
1埼玉県熊谷40.92007-08-16
2岐阜県多治見40.92007-08-16
3山形県山形40.81933-07-25
4山梨県甲府40.72013-08-10

タブ区切りのファイルを読み込みする場合は
read_tableというのが使えます。

1行目からデータなのでヘッダー行がありません。
header=Noneで
ヘッダーの指定もしておきましょう。

次にファイルへの分割です。
データフレームの列で抽出し
to_csvでファイル化できます。
df[[0]].to_csv('col1.txt',index=False,header=False)
df[[1]].to_csv('col2.txt',index=False,header=False)
ファイル出力する際はindexとheaderの指定もしておかないと
出力されてしまうので、
Falseを指定して出力されないようにしておきます。

Linuxコマンドの場合は
これをcutコマンドで実現できます。
cat hightemp.txt | cut -f 1 -d $'\t' > p1.txt
cat hightemp.txt | cut -f 2 -d $'\t' > p2.txt

cutコマンドでは区切り文字を指定して
その何列目を取るかという指定ができます。

コマンド > ファイル名

でファイル出力ができます。



13. col1.txtとcol2.txtをマージ

12で作ったcol1.txtとcol2.txtを結合し,
元のファイルの1列目と2列目を
タブ区切りで並べたテキストファイルを作成せよ.
確認にはpasteコマンドを用いよ.

ファイルの結合問題です。

先ほど作ったファイルを連結させてみましょう。
まずはデータフレームに2つのファイルを読み込みします。
Import pandas as pd
df1 = pd.read_table('col1.txt',header=None)
df2 = pd.read_table('col2.txt',header=None)

データフレームの結合は
pd.concat([データフレーム,データフレーム])
で行います。

行列、どちらの方向で結合するのかを指定しないと
いけないのでaxis=1で列方向にくっつけるのを
指定します。
df3 = pd.concat([df1,df2],axis=1)
df3.to_csv('col3.txt',index=False,header=False,sep='\t')
ファイル出力はto_csvですが
区切り文字を指定しておかないと
タブ区切りにはならないのでsep=区切り文字
で区切り文字を指定しましょう。

Linuxコマンドの場合は
paste -d '\t' col1.txt col2.txt > p12.txt
pasteコマンドでファイル連結の結果出力ができます。
その場合どの区切り文字で連結するかを指定し
最後に結果をファイル出力をします。



14. 先頭からN行を出力

自然数Nをコマンドライン引数などの手段で受け取り,
入力のうち先頭のN行だけを表示せよ.
確認にはheadコマンドを用いよ.

これもデータフレームで簡単に操作できます。
データフレーム変数.head(行数)で
先頭行から抽出できます。

N = int(input())
df = pd.read_table(file_path,header=None)
df.head(N)
2
0123
0高知県江川崎41.02013-08-12
1埼玉県熊谷40.92007-08-16
input()で入力を受け取ることができます。
int()で整数値に変換しています。

Linuxコマンドも同様のheadコマンドで
先頭行から出力できます。
head -3 hightemp.txt
高知県	江川崎	41	2013-08-12
埼玉県	熊谷	40.9	2007-08-16
岐阜県	多治見	40.9	2007-08-16


15. 末尾のN行を出力

自然数Nをコマンドライン引数などの手段で受け取り,
入力のうち末尾のN行だけを表示せよ.
確認にはtailコマンドを用いよ.

これは14問目がtailに変わるだけです。

tailコマンドは末尾から出力できます。



16. ファイルをN分割する

自然数Nをコマンドライン引数などの手段で受け取り,
入力のファイルを行単位でN分割せよ.
同様の処理をsplitコマンドで実現せよ.


ファイル分割ですがいろいろやり方があるので
1例を紹介します。

numpyのsplitを用いて
データフレームを分割します。
import pandas as pd
import numpy as np
N = int(input())
df = pd.read_table(file_path,header=None)
chunks = np.split(df,N)
for i in range(N):
    print(chunks[i])
3
      0     1     2           3
0   高知県   江川崎  41.0  2013-08-12
1   埼玉県    熊谷  40.9  2007-08-16
2   岐阜県   多治見  40.9  2007-08-16
3   山形県    山形  40.8  1933-07-25
4   山梨県    甲府  40.7  2013-08-10
5  和歌山県  かつらぎ  40.6  1994-08-08
6   静岡県    天竜  40.6  1994-08-04
7   山梨県    勝沼  40.5  2013-08-10
      0    1     2           3
8   埼玉県   越谷  40.4  2007-08-16
9   群馬県   館林  40.3  2007-08-16

分割した結果を表示しています。
ファイル出力をしたければ
繰り返しの中でファイル出力を行い
ファイル名を動的に変えれば実現できると思います。



17. 1列目の文字列の異なり

1列目の文字列の種類(異なる文字列の集合)を求めよ.
確認にはsort, uniqコマンドを用いよ.

ユニークカウントの問題です。

さて、こっからはテクニックが必要かなーと思います。
まずはPythonの方から

データフレームに読み込んだ後は
value_countsで
列の値のユニークカウントができます。

1列目を指定してvalue_countsの結果の
index値の個数を求めています。

import pandas as pd
df = pd.read_table(file_path,header=None)
print(len(df[0].value_counts().index))
12

Linuxコマンドでは
まずcutでタブ区切りにした結果を sort し
uniq でユニークカウントします。
cat hightemp.txt |  cut -f 1 -d $'\t' | sort | uniq -c | sort -nr
   3 群馬県
   3 山梨県
   3 山形県
   3 埼玉県
   2 静岡県
   2 愛知県
   2 岐阜県
   2 千葉県
   1 和歌山県
   1 高知県
   1 愛媛県
   1 大阪府


個数だけ出したければwcコマンドを繋げましょう。
cat hightemp.txt |  cut -f 1 -d $'\t' | sort | uniq -c | sort -nr | wc -l
12


18. 各行を3コラム目の数値の降順にソート

各行を3コラム目の数値の逆順で整列せよ
確認にはsortコマンドを用いよ

並び替えの問題ですね。

データフレームではsort_valuesで
並び替えをすることができます。

ソートでは昇順と降順があるので
ascending=Trueで昇順、Falseで降順です。

df = pd.read_table(file_path,header=None)
df.sort_values(2,ascending=True)
0123
23愛知県名古屋39.91942-08-02
21山梨県大月39.91990-07-19
20大阪府豊中39.91994-08-08

データフレームではデータの型があります。
数値型であれば数値の大小での並び替えになります。

Linuxコマンドではsortコマンドで並び替えできます。
並び替えの対象列を指定してます。
!sort -n -k 3 hightemp.txt
山形県	鶴岡	39.9	1978-08-03
山梨県	大月	39.9	1990-07-19
大阪府	豊中	39.9	1994-08-08


19. 各行の1コラム目の文字列の出現頻度を求め,
出現頻度の高い順に並べる


各行の1列目の文字列の出現頻度を求め,
その高い順に並べて表示せよ.
確認にはcut, uniq, sortコマンドを用いよ.

17問目と似ていますが
17問目は各行の値でのユニークカウント
今度はそれぞれの文字でのユニークカウントですね。

まずは1列目の文字を全部抜き出してみましょう。

text = []
with open(file_path) as _f:
    for row in _f:
        text.append(row.split('\t')[0])
texts = ''.join(text)
with 構文でファイル読み込みして
1列目だけをリスト型に格納し
それをjoinで繋げて1つの文字列にします。

for文では文字列型は
1文字ずつ処理できるので
1文字ずつ集計します。

集計は辞書型などで行うことができます。
res = {}
for t in texts:
    if t in res:
        res[t]+=1
    else:
        res[t]=1
最後に辞書型の結果を並び替えて出力です。
for k,v in sorted(res.items(),key=lambda x:x[1],reverse=True):
    print(k,v)
県 23
山 7
知 3
埼 3


結構長くなりましたね。

Linuxコマンドをみてみましょう。

17問目の答えでは1行ずつ処理していましたので
そこに1つコマンドを足します。
cat hightemp.txt |  cut -f 1 -d $'\t' | fold -w 1 | sort | uniq -c | sort -nr
  24 
  23 県
   7 山
   3 馬

fold コマンドは指定した文字数で改行させるコマンドです。
これを用いて1行の文字列を1文字ずつ複数行に変換し
それをユニークカウントしています。


はい
第二章はこれで終わりです。

Linuxコマンドは
Macでは標準で使用できますし
開発現場では普通に使用していると思います。

ログファイルの操作や中身の確認などでも
威力を発揮しますし、1行で済んだりして
とても効率が良いです。

Pythonなどと並行して
覚えておくと自然言語処理がスムーズになるかと思いますので
覚えておいて損はないと思います。

今回はここまでです
それでは。

自然言語処理100本ノック
準備運動の続きです。

解説動画はこちら


自然言語処理100本ノックはこちら
自然言語処理100本ノック


前回はこちら
準備運動1

さて
続きですが

06. 集合

"paraparaparadise"と"paragraph"に含まれる
文字bi-gramの集合を, それぞれ, XとYとして求め,
XとYの和集合,積集合,差集合を求めよ.

さらに,'se'というbi-gramが
XおよびYに含まれるかどうかを調べよ.

文字列の集合を考える問題です。

bi-gramというのはふた文字の組み合わせです。

前回作成したn-gramを作る関数を
用いてbi-gramを作っていきます。
def n_gram(st, n):
    return [st[i:i+n] for i in range(len(st)-n+1)]
pythonでは集合を表現するデータ型があります。
set型は重複を排除したデータ構造です。

リスト型からset関数で作成できるので
bi-gramの結果をそのままsetにします。

X = set(n_gram("paraparaparadise",2))
Y = set(n_gram("paragraph",2))
print(X)
print(Y)
{'ap', 'ad', 'di', 'ra', 'is', 'ar', 'pa', 'se'}
{'ap', 'ph', 'ra', 'ar', 'pa', 'ag', 'gr'}

先頭から二文字ずつ切り取った文字を
集合にしているので重複した部分は
取り除かれます。

Pythonの集合の型同士で
そのまま計算ができます。

和集合、積集合、差集合
文字を含むかどうかの判定は
以下のようなコードになります。

# 和集合 | union
print(X|Y)

# 積集合 & intersection
print(X&Y)

# 差集合 - difference
print(X-Y)

# 含まれるかどうか in
print('se' in X)
print('se' in Y)
{'ap', 'ad', 'di', 'ra', 'ph', 'is', 'ar', 'pa', 'ag', 'gr', 'se'}
{'pa', 'ap', 'ar', 'ra'}
{'is', 'ad', 'se', 'di'}
True
False


07. テンプレートによる文生成

引数x, y, zを受け取り「x時のyはz」という
文字列を返す関数を実装せよ.

さらに,x=12, y="気温", z=22.4として,
実行結果を確認せよ.

恐らくですが
気温を英語に直すと
temperature

なので
template
とかけているんじゃ無いかと!!!!

素敵すぎる問題です。
嫌いじゃ無い、こういうの

ということで
Pythonでは文字列のformatで
文字の差込ができます。

def temperature(x,y,z):
    return '{0}時の{1}は{2}'.format(x,y,z)

temperature(x=12, y="気温", z=22.4)
'12時の気温は22.4'

メールの定型文など
文字の差し込みをする際に非常に便利で
業務でも多用します。

formatは覚えておくと
非常に重宝しますね。

08. 暗号文

与えられた文字列の各文字を,
以下の仕様で変換する関数cipherを実装せよ.
・英小文字ならば(219 - 文字コード)の文字に置換
・その他の文字はそのまま出力
この関数を用い,英語のメッセージを暗号化・復号化せよ.

まずは英小文字かどうかを
判定するという部分です。

これは非常に厄介で
Pythonでは文字列型にそれっぽい関数があるのですが
うまく判定されないという致命的な欠陥があります。

なので判定関数を先に作ります。

reライブラリを用いて
正規表現で英小文字を判定します。

import re

def isalpha(st):
    return re.compile(r'^[a-z]+$').match(st) is not None

この関数を用いると英小文字以外はFalseになります。

この判定結果を用いて
暗号文を作成する関数を作ります。

英小文字の際は
条件として
(219 - 文字コード)の文字に置換 
というのがあります。

文字というのは
全て文字番号が存在し
一文字ずつ番号が降られています。

Pythonでは
ord(文字列)
で文字番号を取得できます。

chr(番号)
で文字を取得できます。

組み合わせると文字を
変換できるというわけです。

最終的にできた関数はこちら。

def cipher(st):
    return ''.join([chr(219-ord(s)) if isalpha(s) else s for s in st])

message = '私の名前は乙pyです'

#暗号化
cipher_text = cipher(message)
print(cipher_text)

#復号化
print(cipher(cipher_text))
私の名前は乙kbです
私の名前は乙pyです

関数の結果は文字が暗号化され
その結果をもう一度関数にかけると
元の文字に復号されます。

09. Typoglycemia

スペースで区切られた単語列に対して,
各単語の先頭と末尾の文字は残し,
それ以外の文字の順序を
ランダムに並び替えるプログラムを作成せよ.
ただし,長さが4以下の単語は並び替えないこととする.

適当な英語の文(例えば
"I couldn't believe that I could actually understand
what I was reading :
the phenomenal power of the human mind ." )
を与え,その実行結果を確認せよ.


これは非常に厄介な問題ですね

考え方はいくつもあり
答えは一つにならないと思いますが
自分のコードはこうなりました。

import random

w = "I couldn't believe that I could actually understand what I was reading : the phenomenal power of the human mind ."

def typoglycemia(w):
    ws = w.split(' ')
    start,end=ws[0],ws[-1]
    base = ['' if len(a)>4 else a for a in ws[1:-1]]
    tmp = [a  for a in ws[1:-1] if len(a)>4]
    random.shuffle(tmp)
    result,c = [start],0
    for b in base:
        if b=='':
            result.append(tmp[c])
            c+=1
        else:
            result.append(b)
    return result + [end]

print(typoglycemia(w))
['I', 'power', 'actually', 'that', 'I', "couldn't", 'reading', 'believe', 'what', 'I', 'was', 'human', ':', 'the', 'understand', 'phenomenal', 'of', 'the', 'could', 'mind', '.']

実行毎に文字の順番は入れ替わります。

この場合の考え方としては
元の文章の四文字以下はそのままに
五文字以上は空白にしたデータを用意します。

別途五文字以上のものをリストに確保します。

元の文章の空白部分に
ランダムに並び替えしたリストを
順番に差し込んでいます。

もう少しスマートに書けるかなと思います
リファクタリングは大事ですね。

さて
これで準備運動は終わりです。

これで
準備運動ですからねー

なかなかパズルのようで
面白いですよね。

続きはまた
それでは。


今回は
自然言語処理100本ノックの準備運動を解いてみました。



解説動画はこちら


自然言語処理とは
コンピューターに言語を処理させるための
一連の方法です

自然言語処理100本ノックはこちら
自然言語処理100本ノック

プログラミングを行う上で
こういった文字列の処理方法を覚えておくと
プログラミングスキルが格段にアップします。

やっておいて損はしないと思います。

どの言語でも対応することは可能です。

さてPython言語を用いて
1つ1つ解いていきましょう。

第1章: 準備運動 からです。

00. 文字列の逆順

文字列"stressed"の文字を逆に
(末尾から先頭に向かって)並べた文字列を得よ.

解き方としては
文字列のリバース方法を考えるです。

Python言語の
リスト型などはリバース用の関数がありますが
文字列には無いようです。

Pythonでは次のようにすることで実現できます。
"stressed"[::-1]
'desserts'

これだけです。
インデックスを用いると文字列を切り出すことができます。

インデックスの書き方は
開始位置 : 終了位置 : 飛ばす個数
になっています。

-1することで終わりから抽出を行います。

01. 「パタトクカシーー」
「パタトクカシーー」という文字列の
1,3,5,7文字目を取り出して連結した文字列を得よ.

文字の奇数番目を取り出す方法です。

Pythonの文字列のインデックスの操作で簡単に取り出せます。
"パタトクカシーー"[0::2]
'パトカー'

これだけですね。
インデックスの最後を2とすることで
1こ飛ばしで文字を抽出できます。

02. 「パトカー」+「タクシー」=「パタトクカシーー」

「パトカー」+「タクシー」の文字を
先頭から交互に連結して文字列「パタトクカシーー」を得よ.

文字を交互に組み上げる問題です。

ここから少し考えるのが必要になりそうですね。
こういうのは業務でも良く出てきそうな問題なので
覚えておくと仕事がはかどりますね。

文字列からの文字の切り出しは
インデックスを用いて
行うことができました。

単純の文字と文字を足すと
文字の連結になってしまいます。

交互にというのがポイントです。

zip関数を使うと2つの文字列やリストなどを組み合わせて
使うことができます。

''.join([p+t for p,t in zip('パトカー','タクシー')])
'パタトクカシーー'

一旦zipで1文字ずつ交互に切り出し、
それを内包表記で書いてあげるとリスト型になります。

文字列型のjoin関数でリスト型を文字列に連結できます。

03. 円周率
"Now I need a drink, alcoholic of course,
after the heavy lectures involving quantum mechanics."
という文を単語に分解し,
各単語の(アルファベットの)文字数を
先頭から出現順に並べたリストを作成せよ.


最終的に欲しいものはリストです。

各単語に分解するというのは
文字列型のsplit関数でスペース区切りで区切ると
実現できます。

アルファベットの・・・
と書かれているので
それ以外はの文字が不要になりますね。

文字列の削除はreplace関数で実現できます。

文章を単語にしたら
欲しいのは文字数ですね。

文字列の文字数はlen関数で測ることができます。

出来上がったコードはこのような形になりました。

w = "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."

[len(s) for s in w.replace(',','').replace('.','').split(' ')]
[3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9]

replaceを二回続けていますが
reライブラリを用いると簡素になります。

import re

w = "Now I need a drink, alcoholic of course, after the heavy lectures involving quantum mechanics."

w = re.sub(r'[.,]', "", w)

[len(s) for s in w.split(' ')]
[3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9]


04. 元素記号
"Hi He Lied Because Boron Could Not Oxidize Fluorine.
New Nations Might Also Sign Peace Security Clause.
Arthur King Can."
という文を単語に分解し,
1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語は先頭の1文字,
それ以外の単語は先頭に2文字を取り出し,
取り出した文字列から単語の位置(先頭から何番目の単語か)
への 連想配列(辞書型もしくはマップ型)を作成せよ.


最終的に欲しいものは辞書型ですね。

まず単語分解は前問と同じようにsplitで行います。

次に考えることは何文字切り出すです。
これも文字列のインデックスで
実現できます。

文字の切り出しに条件がついています。
1,5,6,7,8,9,15,16,19番目だけ1文字
他は2文字

ややこしい条件ですよね。
条件分岐は IF文を用います。

文章から単語に切り分けた段階でリスト型になっています。
リスト型では順番に処理することができるので
そこにenumerate関数を加えると
何番目という数値を取ることができます。

あとは条件分岐とうまく設定すればいいです。

コードはこうなりました。

w = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."

{i+1:s[0] if i+1 in [1,5,6,7,8,9,15,16,19] else s[0:2] for i,s in enumerate(w.split(' '))}

内包表記では
FOR , IF文を組み合わせて使うことができます。

05. n-gram
与えられたシーケンス(文字列やリストなど)から
n-gramを作る関数を作成せよ.

この関数を用い,
"I am an NLPer"という文から
単語bi-gram,文字bi-gramを得よ.

n-gram
任意の文字列や文書を連続したn個の文字や単語で
分割するテキスト分割方法.

特に,nが1の場合をユニグラム(uni-gram)
2の場合をバイグラム(bi-gram)
3の場合をトライグラム(tri-gram)と呼ぶ.

ということでバイグラムを取得するための
関数を作れという問題ですね。

文字列はそのままfor文の繰り返しに用いることができます。

そこからインデックスを用いて
n文字を取得していけば良いという考えです。

コードは非常にシンプルになります。

def n_gram(st, n):
    return [st[i:i+n] for i in range(len(st)-n+1)]

for i in range(1,4):
    print(n_gram("I am an NLPer",i))
['I', ' ', 'a', 'm', ' ', 'a', 'n', ' ', 'N', 'L', 'P', 'e', 'r']
['I ', ' a', 'am', 'm ', ' a', 'an', 'n ', ' N', 'NL', 'LP', 'Pe', 'er']
['I a', ' am', 'am ', 'm a', ' an', 'an ', 'n N', ' NL', 'NLP', 'LPe', 'Per']

単語単位でやりたい場合は
一旦スペースでsplitすれば
単語単位のn-gramになります。

for i in range(1,4):
    print(n_gram("I am an NLPer".split(' '),i))
[['I'], ['am'], ['an'], ['NLPer']]
[['I', 'am'], ['am', 'an'], ['an', 'NLPer']]
[['I', 'am', 'an'], ['am', 'an', 'NLPer']]

まだまだ準備運動は続きますね

今回はここまでです
それでは。



 

このページのトップヘ