感情辞書で繊細な感情分析をする

![スクリーンショット (186).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681665/18443a46-54a5-734f-a81d-bcbddc60ecac.png)

文章の複雑で繊細なニュアンスを汲む感情分析、したいですよね。
できればAIなんて使わず簡単にやりたいですよね。
ということでやってみました。

>### 目次 
>・使う辞書
>・プログラムの流れ
>・ソースコード
>・感じたこと/小ネタ
>・【応用】ネット記事から単語の感情分析

# 使う辞書
今回は感情辞書を使って簡単に感情分析を行います。(感情辞書…言葉とそれに含まれる感情を数値にしたデータ集。いわば国語の教科書のようなもの)
使用したのは以下の２つ。

<b>[・日本語評価極性辞書](https://www.cl.ecei.tohoku.ac.jp/Open_Resources-Japanese_Sentiment_Polarity_Dictionary.html)</b>
ネガポジに分類された「用語編」と、n/p/e(ネガティブ/ポジティブ/ニュートラル)に分類された「名詞編」がある。用語編では5千件、名詞編では8千500件のデータを使用している。

https://www.cl.ecei.tohoku.ac.jp/Open_Resources-Japanese_Sentiment_Polarity_Dictionary.html

<b>[・日本語感情表現辞書](https://sociocom.naist.jp/jiwc-dictionary/)</b>
7つの感情(「悲しい」「不安」「怒り」「嫌悪感」「信頼感」「驚き」「楽しい」)に関する頻出表現を収載している。

https://sociocom.naist.jp/jiwc-dictionary/

ルールベースというと応用の効かないイメージがありますが、人間が周囲の環境や読書からも感情を学ぶように、botにも教科書のようなものは必要でしょう。
これらを使って、文章の感情を分析するプログラムを作ります。

感情の根底には「快・不快(・興奮)」があるので、喜怒哀楽だけでなく快不快も判定することで、汲み取れない絶妙なニュアンスを下から補足できるようにしています。

今回はより応用が利くように7感情で分析していますが、[言語商会の感情辞書](https://www.jnlp.org/GengoHouse/snow/d18")では<b>48もの感情</b>に分類されており、強化学習などの応用を考えていない場合にはこちらの方がより繊細で、おすすめかもしれません。(区分が狭まる分、外す率も増すかも……)

# プログラムの流れ
①Janomeを使って形態素解析をする(文を用語に分ける) ※1
②用語をひとつずつ辞書と照らし合わせて、感情数値を加算する
③「！」の数に応じて乗算する
④数値を検出単語数で割って平均を取る ※2
⑤結果を表示する
→できた！

大事なのは「発した側の感情」分析であり、「受け取る側の感情」分析ではないということです。

※1 [Janomeの分かりやすい説明記事](https://note.nkmk.me/python-janome-tutorial/)
※2文章が長いほど数値が大きくなるのを防ぐため。

# ソースコード
```Python
from janome.tokenizer import Tokenizer
import csv

def textcheck(user,boxname,check=0): #文中にリスト内の単語が含まれるか判定する関数
  for boxword in boxname:
    if user.find(boxword)!=-1:check=check+1
  return check

def userfeeling(user): #ユーザーの感情を読み取る関数(例:■ね→嫌悪,怒り)
  pleasure=0.0
  sad=afraid=angry=hate=trust=surprise=happy=0 #感情の定義
  feel=[sad,afraid,angry,hate,trust,surprise,happy]

  wow=1
  for s in user:
    if s=="!" or s=="！":wow+=1

  happybox=["かわい","好","良","喜","良","楽し","嬉","いい","素敵","おしゃれ"]　#
  sadbox=["悲","かなし","死","失敗","泣","辛","つらい","不安","ごめん"]
  angrybox=["怒","嫌い","殺","苛","イライラ","最低","死ね","馬鹿","たたく","殴る"]
  hatebox=["不快","嫌悪","嫌","目障り","ゴミ","消えろ","死ね","殺","シャットダウン","拷問","たたく","殴る","犯す"]
  afraidbox=["怖","こわい","不安","助けて","ごめん"]
  surprisebox=["びっくり","すごい","驚","意外","えっ","わっ","！？"]
  trustbox=["信","好き","付き合","任せ"]

  for i,box in zip(range(len(feel)),[sadbox,afraidbox,angrybox,hatebox,trustbox,surprisebox,happybox]):
    check=textcheck(user,box,0)
    if check>0:feel[i]+=check*wow

  with open("/content/drive/My Drive/pn.csv.m3.120408.trim") as f: #極性辞書(名詞編)
    textbox=[s.rstrip() for s in f.readlines()]
  with open("/content/drive/My Drive/wago.121808.txt") as f: #極性辞書(用語編)
    textbox2=[s.rstrip() for s in f.readlines()]
  with open("/content/drive/My Drive/JIWC-A_2019.csv") as f: #感情辞書
    reader=csv.reader(f)
    feeldic=[row for row in reader]
  del feeldic[0]

  npwordcount=0
  feelwordcount=0
  t=Tokenizer()
  for token in t.tokenize(user):
    check=0
    if token.surface=="いい":continue #判定無視
    if token.surface=="し":continue
    if token.part_of_speech.find("助詞")!=-1 or token.part_of_speech.find("助動詞")!=-1 or token.part_of_speech.find("非自立")!=-1:continue
    for i in range(len(textbox)): #極性辞書(名詞編)の判定
      word,negaposi,_=textbox[i].split("\t")
      if token.surface==word:
        npwordcount+=1
        check=1
        if negaposi=="e":pleasure+=0*wow
        elif negaposi=="p":pleasure+=0.3*wow
        elif negaposi=="n":pleasure+=-0.3*wow
        elif negaposi=="?p?n":pleasure+=0.1*wow
        break
    if check==0: #↑になかった場合のみ
      for i in range(len(textbox2)): #極性辞書(用語編)の判定
        if "\t"+token.surface in textbox2[i]:
          npwordcount+=1
          negaposi,word=textbox2[i].split("\t")
          if "ポジ" in negaposi:pleasure+=0.3*wow
          elif "ネガ" in negaposi:pleasure+=-0.3*wow
          break
    for i in range(len(feeldic)): #感情辞書の判定
      if token.surface in feeldic[i]:
        feelwordcount+=1
        for i2 in range(len(feel)):feel[i2]+=wow*float(feeldic[i][i2+1])

  if npwordcount!=0:
    pleasure=pleasure/npwordcount
  if feelwordcount!=0:
    for i in range(len(feel)):feel[i]=feel[i]/feelwordcount
  return pleasure,feel

#ーーーここからは表示に関する記述なので、読まなくてよいーーー
textlist=[
"吾輩は猫である。名前はまだ無い。",
"恥の多い生涯を送って来ました。",
"メロスは激怒した。必ず、かの邪智暴虐の王を除かなければならぬと決意した。",
"そうか、そうか、つまりきみはそんなやつなんだな。",
"今日はステキな日だ。花が咲き、鳥が鳴いている。",
"レターパックで現金送れは全て詐欺です。",
]

for s in textlist:
  upleasure,ufeel=userfeeling(s)
  feeldic={0:"悲しみ",1:"恐怖",2:"怒り",3:"嫌悪",4:"信頼",5:"興奮",6:"喜び"}
  print("送信:",s)
  print("想定:",upleasure,ufeel)
  if upleasure==0:print("平常 且つ",end=" ")
  elif upleasure>0:print("ポジティブ 且つ",end=" ")
  elif upleasure<0:print("ネガティブ 且つ",end=" ")
  if (max(ufeel))>=1:howmatch="とても"
  elif (max(ufeel))>=0.6:howmatch="やや"
  elif (max(ufeel))>=0.3:howmatch="ほんのり"
  if max(ufeel)!=0:print(feeldic[ufeel.index(max(ufeel))]+"が"+howmatch+"強い文章です。",end="")
  else:print("無感情です。")
  ufeel[ufeel.index(max(ufeel))]=0
  if max(ufeel)!=0:print("次に"+feeldic[ufeel.index(max(ufeel))]+"が強いです。")
  else:print("")
  print("-------------------------")
```

分けて見ていきます。

これは極性辞書(ネガポジ)2種を読み込むコード。.pnファイルを.txtで保存しなおして使用しています。
用語編は形態素で区切られているので扱いに困りましたが、前にTABをつけて検索することで事なきを得ました。
```Python:極性辞書の読み込み、判定
  with open("/content/drive/My Drive/pn.csv.m3.120408.trim") as f: #極性辞書(名詞編)
    textbox=[s.rstrip() for s in f.readlines()]
  with open("/content/drive/My Drive/wago.121808.txt") as f: #極性辞書(用語編)
    textbox2=[s.rstrip() for s in f.readlines()]
#～～～
    for i in range(len(textbox)): #極性辞書(名詞編)の判定
      word,negaposi,_=textbox[i].split("\t")
      if token.surface==word:
        npwordcount+=1
        check=1
        if negaposi=="e":pleasure+=0*wow
        elif negaposi=="p":pleasure+=0.3*wow
        elif negaposi=="n":pleasure+=-0.3*wow
        elif negaposi=="?p?n":pleasure+=0.1*wow
        break
    if check==0: #↑になかった場合のみ
      for i in range(len(textbox2)): #極性辞書(用語編)の判定
        if "\t"+token.surface in textbox2[i]:
          npwordcount+=1
          negaposi,word=textbox2[i].split("\t")
          if "ポジ" in negaposi:pleasure+=0.3*wow
          elif "ネガ" in negaposi:pleasure+=-0.3*wow
          break
```

これは感情辞書(7分類)を読み込むコード。cvs.readerを使います。
```Python:感情辞書の読み込み、判定
import csv

  with open("/content/drive/My Drive/JIWC-A_2019.csv") as f: #感情辞書
    reader=csv.reader(f)
    feeldic=[row for row in reader]
  del feeldic[0]
#～～～
    for i in range(len(feeldic)): #感情辞書の判定
      if token.surface in feeldic[i]:
        feelwordcount+=1
        for i2 in range(len(feel)):feel[i2]+=wow*float(feeldic[i][i2+1])
```

そして精度を高めるため、感情の特別分かりやすいものは自作辞書を用意し、文全体に完全一致で大きな数値を与えています。
```Python:自作辞書での判定
  happybox=["かわい","好","良","喜","良","楽し","嬉","いい","素敵","おしゃれ"]　#
  sadbox=["悲","かなし","死","失敗","泣","辛","つらい","不安","ごめん"]
  angrybox=["怒","嫌い","殺","苛","イライラ","最低","死ね","馬鹿","たたく","殴る"]
  hatebox=["不快","嫌悪","嫌","目障り","ゴミ","消えろ","死ね","殺","シャットダウン","拷問","たたく","殴る","犯す"]
  afraidbox=["怖","こわい","不安","助けて","ごめん"]
  surprisebox=["びっくり","すごい","驚","意外","えっ","わっ","！？"]
  trustbox=["信","好き","付き合","任せ"]

  for i,box in zip(range(len(feel)),[sadbox,afraidbox,angrybox,hatebox,trustbox,surprisebox,happybox]):
    check=textcheck(user,box,0) #check=1*引っかかった単語の数
    if check>0:feel[i]+=check*wow
```



出力結果は以下のようになりました。
![スクリーンショット (186).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681665/18443a46-54a5-734f-a81d-bcbddc60ecac.png)

# 感じたこと/小ネタ
・統計的には「猫」はネガティブ(猫被り、猫に小判etc…)
・多分「髪が入ってた」などの影響で「髪」がネガティブ判定
・多分「客が多い」などの影響で「多い」もネガティブ判定
・「いい」と「し」は省いた方が良い。

# 【応用】ネット記事から単語の感情分析
![スクリーンショット (190).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681665/ce5eb19e-209d-1a35-17da-4e344c4b4706.png)
調べたい言葉をgoo辞書で検索し、説明文から感情分析を行います。
使うのはRequestsとBeautifulSoup4。pipでインストールします。
```
pip install beautifulsoup4
pip install requests
```
コードは以下の通り。
```Python:単語の説明文を取得
import requests #サイト読み込み
from bs4 import BeautifulSoup #HTML除去

word="希望"
url="https://dictionary.goo.ne.jp/word/"+word
webpage=requests.get(url).text
soup=BeautifulSoup(webpage,"html.parser")
for meta in soup.find_all('meta',attrs={'name':'description'}):allabout=meta.get('content')
about=allabout.split("。")[2]
if ")" in about:about=about.split(")")[1]

upleasure,ufeel=userfealing(about) #説明を先ほどの感情分析関数に通す
#表示に関する記述は省略します。
```

:::note warn
注意！
ループ、短い間隔でアクセスするのはサーバーに負荷をかける行為になってしまうので避けましょう。dos攻撃という犯罪に当たります。
:::
goo辞書のURL＋単語のページをスクレイピングし、説明文が書かれたところ(name="description" の "content")を抜いて、先ほどの感情分析関数に通しています。結果は画像のようになりました。

応用すればツイートから新語/ネットスラングの感情判定を行えるようになるでしょう。スクレイピングは健全な頻度で行いましょう。

<br>
以上です。ありがとうございました。