Tech Blog

Information Technology / Machine Learning / Data Analysis / Big Data / System Integration

Rでテキストファイルをセンチメントの時系列データに変換する

目的

テキストファイルから読み取れるセンチメント(感情)を測定したい。そのために、テキストに現れる各単語のセンチメントを出現順に計測し、時系列データとみてグラフを描く。また、その平均・標準偏差・歪度・尖度などを算出する。

手法

感情辞書

単語と感情を対応付ける辞書として、「単語感情極性対応表」(高村大也, 乾孝司, 奥村学, "スピンモデルによる単語の感情極性抽出", 情報処理学会論文誌ジャーナル, Vol.47 No.02 pp. 627--637, 2006. )を用いる。本辞書では、各単語に対する印象を、positive: +1 ~ negative: -1の「感情極性値」で対応付けている。具体的には、辞書はテキストファイルで、55125個の単語に対して、「見出し語:読み:品詞:感情極性値」が記述されている。 なお、本辞書は研究目的に限り公開されている。

・pn_ja.txtの内容

優れる:すぐれる:動詞:1
良い:よい:形容詞:0.999995
喜ぶ:よろこぶ:動詞:0.999979
褒める:ほめる:動詞:0.999979
めでたい:めでたい:形容詞:0.999645
・・・
ない:ない:助動詞:-0.999997
酷い:ひどい:形容詞:-0.999997
病気:びょうき:名詞:-0.999998
死ぬ:しぬ:動詞:-0.999999
悪い:わるい:形容詞:-1

 

センチメントの計測

感情辞書を用いて、テキストファイルの形態素のセンチメントを割り出し、時系列データとみて、グラフ化・特徴量算出を行う。 

・Rのスクリプト(forループ多用してますが、あまりマネしないように。。)

# 感情辞書の読込み
affectDict <- read.table("C:/LyricsWorkspace/AffectDictionary/pn_ja.txt",sep=":",as.is=TRUE) #因子化を抑制しておく
 
# 歌詞の読込みと形態素解析
library(RMeCab)
dir <- "C:/LyricsWorkspace/Lyrics/Mr.Children_noheader"
lyricsList <- list.files(dir)
for (lyricName in lyricsList) {
 
textFile <- paste("C:/LyricsWorkspace/Lyrics/Mr.Children_noheader/",lyricName,".txt",sep="")
lyric <- RMeCabText(textFile)
 
# 単語を羅列したベクトルを作成する
wordVector <- rep(0,length=length(lyric))
for (i in 1:length(lyric)){
  wordVector[i] <- lyric[ [i]][8]
}
 
# 感情辞書を用いてセンチメントを羅列したベクトルを作成する
sentiVector <- c()
sentiVectorWords <- c()
for (i in 1:length(lyric)){
  cond <- (affectDict$V1 == wordVector[i])
  value <- affectDict[cond,]$V4
  word <- affectDict[cond,]$V1
  if (length(value)>=1) {
    value <- mean(value) #該当する単語が辞書に複数存在する場合は平均をとる
    word <- word[1]
    sentiVector <- append(sentiVector,value)
    sentiVectorWords <- append(sentiVectorWords,word)
  }
}
names(sentiVector) <- sentiVectorWords
 
# 特徴量の算出と保存
features <- list(0,0,0,0,c(0,0,0,0,0))
names(features) <- c("mean","sd","skew","kurto","quant")
features[ ["mean"]] <- mean(sentiVector) #平均
features[ ["sd"]] <- sd(sentiVector) #標準偏差
features[ ["skew"]] <- mean( (sentiVector-mean(sentiVector))^3)/(sd(sentiVector)^3) #歪度
features[ ["kurto"]] <- mean( (sentiVector-mean(sentiVector))^4)/(sd(sentiVector)^4) #尖度
features[ ["quant"]] <- quantile(sentiVector) #4分位偏差
write.table(unlist(features), paste("C:/LyricsWorkspace/TimeSeries/Features5_union/",lyricName,".txt",sep=""))
 
# グラフのプロットと保存
jpeg(paste("C:/LyricsWorkspace/TimeSeries/WithWords/",lyricName,".jpeg",sep=""), width=1440, height=960)
plot(sentiVector,type="l",ylim=c(-1.1,1.1))
par(ps=25)
text(1:length(sentiVector),sentiVector,names(sentiVector))
dev.off()
 
}

 

結果

Mr.ChildrenのMarshmallow dayを例に、出力結果を掲載する。縦軸にセンチメント(印象のpositive / negative)を、横軸にテキスト内での単語の順序をとると、以下のようなグラフが出力される。

f:id:tkdmah:20130113121129j:plain

また、特徴量として、以下のテーブルが出力される。

f:id:tkdmah:20130113121335j:plain

Rで複数のテキストファイルを二次元にマッピングして可視化する

目的

複数のテキストファイルの関係性をコンパクトに表現し、可視化したい。

 

手法

自己組織化マップ(SOM)

自己組織化マップは、視覚野のニューラルネットのモデルを元にした学習アルゴリズムであり、高次元データを低次元に非線形射影して表示できる。
http://ja.wikipedia.org/wiki/%E8%87%AA%E5%B7%B1%E7%B5%84%E7%B9%94%E5%8C%96%E5%86%99%E5%83%8F

・Rスクリプト

# 入力データのテーブルを読込み
a <- read.table("C:/LyricsWorkspace/DocMatrix/tf3idf4/Mr.Children_single.txt") 
# ライブラリのインポート
install.packages("kohonen")
library(kohonen) 
# SOMの実行
set.seed(10) #シードを指定
gr <- somgrid(topo="hexagonal", xdim=3, ydim=3) #蜂の巣上に3x3の出力層を形成, topo=c("hexagonal","rectangular")
a.som <- som(t(a), gr, rlen=100) #入力層a, 出力層grに対して100回学習
# 結果のプロット
# plot.kohonen(a.som, type="codes") #コードマップ
plot.kohonen(a.som, type="mapping",labels=colnames(a)) #個体のマップ 

 

主成分分析(PCA)

主成分分析とは、次元を主要な成分に縮約して表現する手法であるが、可視化の1つであると見ることもできる。

・Rスクリプト

# 入力データのテーブルを読込み
a <- read.table("C:/LyricsWorkspace/DocMatrix/tf3idf4/Mr.Children_single.txt") 

# ライブラリのインポート
library(stats)

# 主成分分析を実行
a.pc <- prcomp(t(a))summary(a.pc) #princomp()は(行数)<(列数)で実行不可だが、prcomp()では可能
a.pc$sumarry #サマリ表示
a.pc$rotation #固有ベクトル表示

# 結果のプロット
plot(a.pc$x[,1],a.pc$x[,2],type="n")
text(a.pc$x[,1],a.pc$x[,2],colnames(a))

Rでの主成分分析は以下に詳しい。
http://aoki2.si.gunma-u.ac.jp/R/prcomp2.html

 

結果

以下のように可視化される。

自己組織化マップ(SOM)

f:id:tkdmah:20130111020032j:plain

主成分分析(PCA)

f:id:tkdmah:20130111020057j:plain

Mr.Childrenの歌詞分析(5): フレーズに着目した、シングル曲のクラスタリング

主結果

Mr.Childrenのシングル曲を、歌詞のフレーズに着目してクラスタリングしました。頻出フレーズを書き加えています。

 

 f:id:tkdmah:20130106232556p:plain

考察

N-gramでの解析では、フレーズがそのまま残るので、結果を見て解釈しやすいですね。全体として、Mr.Childrenの大きなテーマは「生きる」や「自分」であるようです。

  • 「君がいた夏」「Sign」「【es】~Theme of es~」「マシンガンをぶっ放せ」「旅立ちの唄」「抱きしめたい」「Replay」「口笛」はラブソングで、「hypnosis」「優しい歌」はその中でも儚さや躊躇いを表現。
  • 「箒星」「HERO」「終わりなき旅」「Everything(It's you)」「くるみ」「NOT FOUND」「君が好き」「Any」「innocent world」「fanfare」「GIFT」は、自分を探している歌。
  • 「しるし」「CROSS ROAD」「I'LL BE」「ニシエヒガシエ」「光の射す方へ」「祈り~涙の軌跡」「花」「かぞえうた」「掌」「Tomorrow never knows」「フェイク」「シーソーゲーム~勇敢な恋の歌~」「名もなき詩」「youthful days」「花の匂い」はぼくらが生きているということ、「everybody goes~秩序のない現代にドロップキック」「HANABI」は生きること、愛することがテーマ。

手法

フレーズに着目するために、N-gramで特徴行列を作成し、クラスタリングを行います。tf*idf行列では形態素解析で単語を抽出するため、単語の前後関係やフレーズといったものは無視されますが、N-gramでは単語で分割されないため、フレーズを抽出することができます。

f:id:tkdmah:20130106233920p:plain

詳しくは以下の記事をご覧ください。

 

Mr.Childrenの歌詞分析(4): 単語の意味を考慮した、シングル曲のクラスタリング

主結果

Mr.Childrenのシングル曲を、歌詞を元にクラスタリングしてみました。 

1.単語ベースのクラスタリング

f:id:tkdmah:20130105213820p:plain

2.概念ベースのクラスタリング

f:id:tkdmah:20130105233845p:plain

 

考察

評価

共通する単語や概念を赤字で書いてみました。単語ベースでは、「時代」「社会」や「幸せ」についてきちんとクラスタが形成されていて、いいかんじです。一方で、概念ベースはパッとしないですね。。正解がないので評価はしにくいですが、どちらかと言えば単語ベースのほうがしっくりくる気がします。本来であれば、好調/不調、前向き/後向き、恋愛/社会といった形で分かれてほしかったのですが。さらに言えば、「君が好き」と「抱きしめたい」、「Sign」と「しるし」がくっついてほしいところでした。(関係ないですが、「Mr.Children「Sign」から「しるし」についての解説」では、「Sign」と「しるし」の関係が記号論を交えて述べられていて、とてもおもしろいです。) 

課題

歌詞のクラスタリングの難しさは、2つあると思います。

1つは、テキストが短いため、1つのドキュメントにおける情報量が少ないことです。テキストが短いと、tf*idf行列が疎になり、テキスト間の距離が均一となってしまいます。

もう1つは、期待するクラスタリング結果が、「水」や「光」といった分類ではなく、「期待」「悲観」「勇気」といった、より心理的な分類であるということです。

改善策

これらの問題を克服するためには、やはり2つの方策をとる必要があると考えています。

1つは、テキスト間の距離を計算するのに、単語同士の距離を考慮して計算することです。今回の手法では、「愛」と「幸福」の距離と、「愛」と「冷蔵庫」の距離は同じです。ところが、本来、これらの心理的距離は異なります。テキストが短く、情報量が少ないことをカバーするために、単語間の距離を考慮する必要があります。具体的には、WordNetのネットワークを利用すればよいと思います。

もう1つは、単語に感情を紐付けて解析することです。心理的な分類を行うためには、前もってどのような単語がどのような感情に関係するのかを知っておく必要があります。そのために、感情辞書と呼ばれるような辞書を別途用いることが考えられます。実際に、英語ではWordNet-Affectという感情を考慮したWordNetがあります。残念ながら日本語版はありませんが、類似の試みはあるようです。

今回はこれで一区切りにしますが、まだまだ改善の余地はあります。ひとまず、機械にすべて解かせるのではなく、軸の設定など人間の手も適度に介在させる必要がありそうです。また時間があれば、チャレンジしてみます。何かアドバイス等あれば、コメントお願いします^^

手法

解析は、以下のように行いました。歌詞のテキストファイルからRMeCabを用いてtf*idf行列を作成し、単語ベースの場合はそのままクラスタリングしました。概念ベースの場合は、WordNetを用いて概念ベースのtf*idf行列に変換してから、クラスタリングしました。クラスタリングでは、局所的重みに対数化索引語頻度、大域的重みにエントロピーを用いました。

f:id:tkdmah:20130105214619p:plain

詳しくは以下の記事をご覧ください。

 

Mr.Childrenの歌詞分析: ここまでのまとめ

主結果

 

手法

PythonでWordNetを利用して、テキストの特徴行列を単語ベース→概念ベースに変換する

目的

「RとRMeCabでテキストファイルをクラスタリングする」 では、各テキストから単語を抽出し、各単語の出現頻度を元に、各テキストの特徴ベクトルを生成した。このとき、例えば"愛"と"あい"という単語は同一のものとして扱われたが、"愛"と"恋"という単語はまったく別のものとして扱われた。この結果、テキスト間の距離が人間が感じる距離と異なる場合が出てきた。そこで、本稿では、同じ概念の単語をひとまとめにして特徴ベクトルを作成することで、概念レベルでのテキストのクラスタリングを目指す。
f:id:tkdmah:20130105151528p:plain  

手法

クラスタリングの手順は以下のとおり。手順2以外については、以前の記事とほぼ同様なので、ここでは詳細は割愛する。

  1. Rで複数のテキストファイルからtf*idf行列を作成し、テキストファイルに出力する。
  2. tf*idf行列の書かれたテキストファイルを、PythonWordNetを用いて変換し、テキストファイルに出力する。
  3. 再びRで修正されたtf*idf行列を読込み、クラスタリングを行う。

手順2では、単語ベースのtf*idf行列を、概念ベースのtf*idf行列に変換する。同じ概念の単語をひとまとめにするために、WordNetを用いる。WordNetとは、単語がその意味でグルーピングされ、それらグループの関係性が記述された、概念辞書である。日本語WordNetはver0.9が2009年に公開され、無償で使用、複写、改変、頒布することが許可されている。また、Perl, Python, Java, Ruby等のフロントエンドも用意されている。今回は、Pythonを用いてWordNetを扱う。(このせいで文字コードに大変苦労させられたわけだが…。)WordNetPythonから使うには、Python2.6(Python3系は不可), sqlite3, WordNet-0.9が必要。

f:id:tkdmah:20130105151532p:plain

Pythonのスクリプトは以下のように書ける。
・wndriver.py: WordNetの自作ドライバ(WordNetのPythonAPIであるwn.pyを利用)

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import wn
 
def getSenseId(argv):
  '''単語を引数とし、単語の概念IDのリストを返す関数'''
  words = wn.getWords(argv.decode('utf-8'))
  senseIds = [ ]
  if words:
    senses = wn.getSenses(words[0])
    for sense in senses:
      senseIds.append(sense[0])
  return senseIds

・convDocMatrix.py: 特徴行列を変換するメインモジュール

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import wndriver 

# prepare file
fin = codecs.open('C:/LyricsWorkspace/DocMatrix/Mr.Children_single_utf8.txt','r',"utf_8")
fout = codecs.open('C:/LyricsWorkspace/DocMatrix/Mr.Children_single_utf8_rev.txt','w',"utf_8")
 
# function definition
def add(a,b):
  c = [ ]
  for i in range(len(a)):
    c.append(float(a[i])+float(b[i]))
  return c
 
# make lyricsList
line = fin.readline()
fout.write(line)
lyricsList = line[1:-3].split('" "')
  
# read lines and get senseids
docDic = {} 
while True:
  line = fin.readline()
  if not line:
    break
  tfidfList = line[:-2].split(' ')
  word = tfidfList[0]
  if not word:
    break 
  senseIds=wndriver.getSenseId(word[1:-1].encode('utf-8'))
  length = len(senseIds)
  for senseId in senseIds:
    if senseId in docDic:
      docDic[senseId] = add(docDic[senseId],[float(x)/length for x in tfidfList[1:]])
    else:
      docDic[senseId] = [float(x)/length for x in tfidfList[1:]]

# write file 
for key in docDic.keys():
  fout.write('"'+key+'" ')
  for score in docDic[key]:
    fout.write(str(score)+' ')
  fout.write('\r\n') 
fin.close()
fout.close()

参考ページ

Mr.Childrenの歌詞分析(3): シングル曲のクラスタリング

主結果

Mr.Childrenのシングル曲の歌詞をクラスタリングしてみると、このようになりました。(アルバム曲を含めてクラスタリングすると図が煩雑になってしまうため、今回はシングル曲のみで実行しました。)

f:id:tkdmah:20130102231629j:plain

 

妥当性の検証

この結果って、どうなんでしょう。クラスタリングの妥当性はどの程度あるのでしょうか。近いと判断されたクラスタごとに、楽曲の歌詞を少し調べてみました。

【街】クラスタ

曲名歌詞
innocent world 黄昏のを背に 抱き合えたあの頃が 胸をかすめる
旅立ちの唄 君の大好きだった歌 に流れる

【優しい】クラスタ

曲名歌詞
口笛 優しく響く あの口笛のように
優しい歌 優しい歌 忘れていた 誰かの為に 小さな火をくべるよな

【現実】クラスタ

曲名歌詞
マシンガンをぶっ放せ この現実に目を向けなさい
hypnosis もう現実から見捨てられても 構わないさ

【明日】クラスタ

曲名歌詞
フェイク 虚しさを抱えて 夢をぶら下げ 二階建ての明日へとTAKE OFF
Tomorrow never knows 心のまま僕はゆくのさ 誰も知ることのない明日

【二人】クラスタ

曲名歌詞
Replay 二人で駆け抜けた季節も どんな場面も 振り返れば ほら 微笑み溢れてる
抱きしめたい 二人だけの 夢を胸に 歩いてゆこう
 
こう見ると、各クラスタは特徴的なキーワードを共有していることが分かります。ただ、現状ではキーワードの意味合いやその状況が解析できていないため、人間がこれらの歌詞を読んだときの感情とは少し離れているようです。
 

手法

本稿は、 前回書いた記事(tf-idf法とウォード法による手法) に基づいて解析しています。