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

自然言語処理

TKO木下の動画が絶賛炎上中だと聞きつけまして
早速コメントを感情分析してみました

解説動画はこちら


炎上している動画はこんな感じです。

動画タイトル:TKO木下よりご報告がございます。
公開日:2020/04/01
2020/04/11 16:00現在
視聴回数:2,845,395 回視聴
高評価:4771件
低評価:28万件
コメント数:50,898 件

素晴らしい低評価の数ですねー。
私は動画は見ておりませんが
低定評に1評投じさせていただきました。


さて
感情分析のやり方ですが
Youtubeのコメントと形態素解析
それと単語感情極性対応表というものを使います。

単語感情極性対応表は
こちらを参照してください。



中身はこんな感じで

ない:ない:助動詞:-0.999997
酷い:ひどい:形容詞:-0.999997
病気:びょうき:名詞:-0.999998
死ぬ:しぬ:動詞:-0.999999
悪い:わるい:形容詞:-1

ポジティブワード、ネガティブワードがあり
ポジティブ方向になると0から1点
ネガティブ方向は-1から0点の間の数値を取ります。

単語のポジネガ具合が分かる訳です。

手順としては

1.事前にYoutubeのコメントを集める。
  Youtubeの動画のページをスクロールして
  コメント表示させてから保存する。

 2.BeautifulSoupでHTMLの中から
  コメントの部分だけ拾い上げる。

3.コメントを形態素解析して単語感情極性対応表と
  比較してポジネガ点数をつける
  ポジティブだとプラス,ネガティブだとマイナスの点になる

まずはじめに
動画のコメントを集めなければならないので
動画のページをブラウザーで開いて
コメントを表示させたら、HTMLファイルに保存します。

次にPythonのコードを使って
HTMLの構文解析を行い、コメントを抽出します。

まず必要なライブラリは
from bs4 import BeautifulSoup
from janome.tokenizer import Tokenizer
t = Tokenizer()

janomeは後に使いますが、インストールしていない方は
pip などでインストールしておきましょう。

次にhtmlファイルを読み込んで変数に格納しておきます。
with open('htmlのファイル') as _f:
    html = _f.read()


お次はコメント部分の抽出です。
htmlファイルの中からコメントを抽出して
リストに格納しています。
soup = BeautifulSoup(html ,'lxml')
contents = soup.find('div',id='contents')
ytds = contents.find_all('ytd-comment-thread-renderer')
result_list = []
for ytd in ytds:
    span1 = ytd.find('span',class_="style-scope ytd-comment-renderer")
    name = span1.text.replace('\n','').replace(' ','') if span1 is not None else ''
    span2 = ytd.find('span',id="vote-count-middle")
    high = span2.text.replace('\n','').replace(' ','') if span2 is not None else ''
    come1 = ytd.find('yt-formatted-string', id="text")
    rep = come1.text.replace(' 件の返信を表示','').replace(' ','') if come1 is not None else ''
    come2 = ytd.find('div',id="content", class_="style-scope ytd-expander")
    com = come2.text.replace('\n','').replace(' ','') if come2 is not None else ''
    tmp = [name,high,rep,com]
    result_list.append(tmp)

次は形態素解析です。

単語感情極性対応表は事前にダウンロードしておいて
CSVファイルなどにしておいてください。

単語を辞書型に取り込みます。
posinega = {}
with open('posinega.csv') as _f:
    for row in _f:
        rows = row.replace('\n','').split(':')
        posinega[rows[0]] = float(rows[3])

最後にコメントを形態素解析して
ポジネガで点数をつけていきます。
nega_result = {}
for rows in result_list:
    count = 0
    for token in t.tokenize(rows[3]):
        h = token.part_of_speech.split(',')[0]
        if any(['名詞'==h,'形容詞'==h,'動詞'==h]):
            num=len(token.surface)
            key = token.surface
            if key in posinega:
                count+=posinega[key]
    nega_result[rows[3]]=count

これでコメントにポジネガの点数が付きました。

最もネガティブなコメントは・・・

-53.76871590000002
私は叙々苑でバイトをしていた者です。
1年前、TKO木下が私のバイト先東京の叙々苑を
訪れた際の記憶は忘れられません。 悪い意味で。
TKO木下は入店早々に既に様子がおかしく、
ホール担当だった私の胸ぐらを急に掴み一言。
「さっさと案内しろやボケ」と。
しかし木下が入店する所を見て3秒程で
駆けつけたのにも関わらすこの暴言暴力
・・・
辛いです。

と全文はかなり長いので端折りますが
冒頭を見るだけでもかなりネガティブさが伝わる文面です。

ネガティブの一番悪いものでも-1点なので
60個近くネガティブな単語が使われているコメントでした。

これだけの長文を想像で書くのは無理でしょうから
実体験だったんだろうと思いますね。

というようなネガティブコメントが多数並んでいました。

逆に
ポジティブなコメント有るんかな?
みてみましょう。

11.14069 ウケる笑笑低評価笑笑笑笑笑笑笑笑笑笑
2.890590 おはよう。くっさいなぁ~笑笑笑
2.530665 祝!低評価日本一更新!
1.666281 2倍速だと面白い
1.412733 朗報低評価日本一達成
1.374283 むしろコメ欄見にきてる笑日本一の低評価王にしてあげるためにみんな低評価押そうな
1.299542 宮迫博之の真似すれば売れるとおもったんやろ笑笑
1.261103 (*'∇')/゚・:*【祝】*:・゚\('∇'*)低評価日本一!よっ!!あっぱれ!流石です!!
0.989242 ヒカルを越した天才
0.989199 適当にやってもいいよ君が面白いと思っても多分面白くないから
0.989199 ff7本当に面白いよ😃
0.963530 なんかこんだけ荒れてることすらおいしいな〜とか思ってそう。笑 0.935754 大喜利コメ見てるから黙れ
0.932040 バチスタすごい好きでしたw
0.932040 本当にお笑いが好きならやらんやろアホか
0.921042 うちの姪が初めて掴まり立ちしましてね。みんなで喜びました。
0.769555 来世で頑張って下さい❗️来世で応援します‼️
0.650668 ファンのみなさま、、、っておるんか?(笑)
0.568479 低評価日本一おめでとう㊗️あと、誰に謝ってんねん
0.568479 おめでとう低評価日本一‼️
0.568479 低評価日本一おめでとうございます!
0.568479 低評価日本一おめでとナス!


低評価おめでとう、など
悪い意味で褒めてる感じですねwwwwww。

というわけで、まとめですが

日頃の行いは大事です。
見てない所でも敬意を払うことは当たり前だと思います。
特にタレントさんの私生活には
その人となりが出るようですね。

また素行の悪い方がテレビに出るのは
あまり良いことだとは思いません。

ネガティブなイメージの有るタレントをCMなどに使うと
商品、サービスにもネガティブイメージがつくので
我々企業側の人間はこういうタレントは
使わないようにしないといけないです。

ということで動画コメントの
感情分析でした。

皆様も
日頃の行いには気をつけたほうが良いですよ
誰がみてるか分かりませんし。

それでは


グッバイ、イレーーギュラーーーーー
とか叫びたくなる今日この頃

なんとなく
Official髭男dismの歌詞の特徴を見てみたい
と思ってしまいました。

解説動画はこちら





はい
ひげだんさんの歌詞の特徴を見ていきたいですが
事前準備として
1.歌詞を集める
2.形態素解析する
3.テキストファイルにする

この作業が必要でーーーす。

これができましたら
以下のコードを試すことができます。

先ずは
ワードクラウドで歌詞の特徴を可視化する。

ワードクラウドは歌詞に出てくる単語の出現頻度順で
文字を大きさを決めて、1枚にまとめたものです。

コードは
from wordcloud import WordCloud
text_file = open('higedan_list.txt', encoding='utf-8')
text = text_file.read()

#fpath = 'C:/Windows/Fonts/meiryob.ttc'
fpath = '/System/Library/Fonts/ヒラギノ丸ゴ ProN W4.ttc'
stop_words = []
#stop_words = ['そう', 'ない', 'いる', 'する', 'まま', 'よう', 'てる','しまう','それ','なる', 'こと', 'もう', 'いい', 'ある', 'ゆく', 'れる']
wordcloud = WordCloud(background_color='white',font_path=fpath,width=800, 
  height=600, stopwords=set(stop_words),
                      regexp=r"[\w']+"
                     ).generate(text)
wordcloud.to_file('higedan.png')
plt.figure(figsize=(12,9))
plt.imshow(wordcloud)
plt.axis("off")
plt.show()

このコードではfpathでフォントの場所を指定しているので
お使いのマシンのパスにあわせてください。

ワードクラウドの結果は
higedan

する、僕、君
が大きいですねーーー

「僕と君がする」んでしょうね!!!!

何をするかは存じませんが
割と一般的な語句が多いようですね。

歌詞の特徴としてはそこまで
突拍子もない単語は連呼していない訳です。

次に単語の数ではなく
TF-IDFで重要そうな言葉を見てみます。

TF とは Term Frequency の略で、単語の出現頻度を表します。
IDF とは Inverse Document Frequency の略で、逆文書頻度を表します。

これを掛け合わせたものがTF-IDFで

ある単語が多くの文書で出現するほど値は下がり
特定の文書にしか出現しない単語の重要度を上げる役割をします。

TF-IDF値は0-1の間の値を取ります。
重要なキーワードは1に近づきます。

0.25より大きなワードを出してみました。

import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
np.set_printoptions(precision=2)

with open('higedan_list.txt','r') as _f:
    docs = [row.replace('\n','') for row in _f]

vectorizer = TfidfVectorizer() 
X = vectorizer.fit_transform(docs)

words = vectorizer.get_feature_names()
for did, vec in zip(range(len(docs)), X.toarray()):
    print('doc id : ', did)
    for w, tfidf in sorted(enumerate(vec), key=lambda x: x[1], reverse=True):
        if tfidf>0.25:
            wo = words[w]
            print('\t{0:s}\t{1:f}'.format(wo, tfidf))
doc id :  1
	love	0.721728
      ・・・
doc id :  43
	違う	0.329506
	グッ	0.327694
	バイ	0.296629
	もっと	0.265779

doc id1は最近佐藤健さんが主演したドラマの主題歌です。

loveという単語は
他の歌詞にはあまり使われていないけど
ここの歌詞では連呼されてるんでしょうねーー

tf-idf 閾値0.1にすると
「イレギュラー」も含まれてきます。

doc id43は映画の主題歌になった曲です。
「グッバイ」が特徴的な感じしますね。

最後にn-gramをみてみます。

n-gramとはn個の単語の繋がりを表す言葉で
3つの単語であればtrigramになります。
with open('higedan_list.txt','r') as _f:
    docs = [row.replace('\n','').split(' ') for row in _f]

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

counts = {}
for doc in docs:
    trigrams = n_gram(doc,3)
    for tri in trigrams:
        key = ''.join(tri)
        if key in counts:
            counts[key] +=1
        else:
            counts[key] = 1
for k,v in sorted(counts.items(),reverse=True,key=lambda x:x[1]):
    print(k,v)
metellme 16
StandByYou 15
tellmetell 14
・・・・

一番多かったのは英語の歌詞の繋がりでした。
かなり英語の歌詞を多用している感じがあります。

まとめると

・全体的に割と一般的な日本語を多く使用する。
・1つの歌詞でキーワードになるような単語がある
「グッバイ」「イレギュラー」
・割と英語を多用するところもある。

かなーと思います。

歌詞の特徴を見るには
他にもいろんな手法があると思いますが
比較的簡単な方法をご紹介しました。

皆さんもぜひ試してみてください。
それでは。

本日は自然言語処理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']]

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

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



 

このページのトップヘ