Сгенерируйте Lexicon специфичного для области настроений

В этом примере показано, как сгенерировать лексикон для анализа настроений с помощью 10-K и 10-Q финансовых отчетов.

Анализ настроений позволяет вам автоматически суммировать настроения в заданном фрагменте текста. Для примера присвойте кусочки текста «Эта компания демонстрирует сильный рост». и «Эта другая компания обвиняется в введении потребителей в заблуждение». с положительными и отрицательными настроениями, соответственно. Также, например, присвоить тексту «Эта компания показывает чрезвычайно сильный рост». более сильный счет настроения, чем текст «Эта компания показывает сильный рост».

Алгоритмы анализа настроений, такие как VADER, полагаются на аннотированные списки слов, называемые лексиконами настроений. Например, VADER использует лексикон настроений со словами, аннотированными с счетом настроения в диапазоне от -1 до 1, где счета, близкие к 1, указывают на сильные положительные настроения, счета, близкие к -1, указывают на сильные отрицательные настроения и счета, близкие к нулю, указывают на нейтральные настроения.

Чтобы проанализировать настроения текста с помощью алгоритма VADER, используйте vaderSentimentScores функция. Если лексикон настроения, используемый vaderSentimentScores функция не подходит для данных, которые вы анализируете, для примера, если у вас есть набор данных для конкретной области, такой как медицинские или инженерные данные, то вы можете сгенерировать свой собственный пользовательский лексикон настроений с помощью небольшого набора seed слов.

В этом примере показано, как сгенерировать лексикон настроения, заданный набор начальных слов, с помощью основанного на графе подхода, основанного на [1]:

  • Обучите встраивание слова, которое моделирует сходство между словами, используя обучающие данные.

  • Создайте упрощенный графиков, представляющий вложение с узлами, соответствующими словам и ребрам, взвешенным подобием.

  • Чтобы определить слова с сильной полярностью, идентифицируйте слова, соединенные с несколькими начальными словами через короткие, но сильно взвешенные пути.

Загрузка данных

Загрузите 10-K и 10-Q данные финансовых отчетов из Комиссии по ценным бумагам и биржам (SEC) через электронный API сбора, анализа и извлечения данных (EDGAR) [2] с помощью financeReports вспомогательная функция, присоединенная к этому примеру в качестве вспомогательного файла. Чтобы получить доступ к этому файлу, откройте этот пример как Live Script. The financeReports функция загружает 10-K и 10-Q отчеты за указанный год, квартал и максимальную длину символов.

Загрузите набор из 20 000 отчетов за четвертый квартал 2019 года. В зависимости от размеров отчетов это может занять некоторое время.

year = 2019;
qtr = 4;
textData = financeReports(year,qtr,'MaxNumReports',20000);
Downloading 10-K and 10-Q reports...
Done.
Elapsed time is 1799.718710 seconds.

Задайте наборы положительных и отрицательных начальных слов для использования с этими данными. Начальные слова должны появиться хотя бы один раз в текстовых данных, в противном случае они игнорируются.

seedsPositive = ["achieve" "advantage" "better" "creative" "efficiency" ...
    "efficiently" "enhance" "greater" "improved" "improving" ...
    "innovation" "innovations" "innovative" "opportunities" "profitable" ...
    "profitably" "strength" "strengthen" "strong" "success"]';

seedsNegative = ["adverse" "adversely" "against" "complaint" "concern" ...
    "damages" "default" "deficiencies" "disclosed" "failure" ...
    "fraud" "impairment" "litigation" "losses" "misleading" ...
    "omit" "restated" "restructuring" "termination" "weaknesses"]';

Подготовка текстовых данных

Создайте имена функции preprocessText который подготавливает текстовые данные для анализа. The preprocessText функция, перечисленная в конце примера, выполняет следующие шаги:

  • Удалите все URL-адреса.

  • Токенизируйте текст.

  • Удалите лексемы, содержащие цифры.

  • Преобразуйте текст в нижний регистр.

  • Удалите любые слова с двумя или меньшим количеством символов.

  • Удалите все стоповые слова.

Предварительно обработайте текст с помощью preprocessText функция. В зависимости от размера текстовых данных это может занять некоторое время.

documents = preprocessText(textData);

Визуализируйте предварительно обработанные текстовые данные в облаке слов.

figure
wordcloud(documents);

Обучите встраиванию слов

Вложения в Word сопоставляют слова в словаре с числовыми векторами. Эти вложения могут захватывать семантические детали слов, так что подобные слова имеют похожие векторы.

Обучите встраивание слова, которое моделирует сходство между словами, используя обучающие данные. Задайте контекстное окно размера 25 и отбросьте слова, которые появляются менее 20 раз. В зависимости от размера текстовых данных это может занять некоторое время.

emb = trainWordEmbedding(documents,'Window',25,'MinCount',20);
Training: 100% Loss: 1.44806  Remaining time: 0 hours 0 minutes.

Создайте График

Создайте упрощенный графиков, представляющий вложение с узлами, соответствующими словам и ребрам, взвешенным подобием.

Создайте взвешенный график с узлами, соответствующими словам в словаре, ребрами, обозначающими, находятся ли слова в пределах неотрицательности 7 друг от друга, и весами, соответствующими косинусному расстоянию между соответствующими векторами слов во вложении.

Для каждого слова в словаре найдите ближайшие 7 слов и их косинусоидные расстояния.

numNeighbors = 7;
vocabulary = emb.Vocabulary;
wordVectors = word2vec(emb,vocabulary);

[nearestWords,dist] = vec2word(emb,wordVectors,numNeighbors);

Чтобы создать график, используйте graph функция и задайте парные исходный и целевой узлы, и задайте их веса кромок.

Определите исходный и целевой узлы.

sourceNodes = repelem(vocabulary,numNeighbors);
targetNodes = reshape(nearestWords,1,[]);

Вычислите веса кромок.

edgeWeights = reshape(dist,1,[]);

Создайте график, соединяющее каждое слово с помощью neigbors с весами ребер, соответствующими счетами подобия.

wordGraph = graph(sourceNodes,targetNodes,edgeWeights,vocabulary);

Удалите повторяющиеся ребра с помощью simplify функция.

wordGraph = simplify(wordGraph);

Визуализируйте раздел графика слов, связанный со словом «потери».

word = "losses";
idx = findnode(wordGraph,word);
nbrs = neighbors(wordGraph,idx);
wordSubgraph = subgraph(wordGraph,[idx; nbrs]);
figure
plot(wordSubgraph)
title("Words connected to """ + word + """")

Сгенерируйте счета настроения

Чтобы определить слова с сильной полярностью, идентифицируйте слова, соединенные с несколькими начальными словами через короткие, но сильно взвешенные пути.

Инициализируйте массив счетов настроения, соответствующих каждому слову в словаре.

sentimentScores = zeros([1 numel(vocabulary)]);

Итеративно пройдите по графику и обновите счета настроений.

Пройдите график на разных глубинах. Для каждой глубины вычислите положительную и отрицательную полярность слов, используя положительные и отрицательные начальные значения для распространения настроений на остальную часть графика.

Для каждой глубины:

  • Вычислите положительные и отрицательные счета полярности.

  • Учитывайте различие в общей массе положительного и отрицательного потока в графике.

  • Для каждого узла-слова нормализуйте различие двух его счетов.

После запуска алгоритма, если фраза имеет более высокий положительный, чем отрицательный счет полярности, то его конечная полярность будет положительной, а отрицательная - в противном случае.

Задайте максимальную длину пути 4.

maxPathLength = 4;

Итеративно пройдите график и вычислите сумму счетов настроения.

for depth = 1:maxPathLength
    
    % Calculate polarity scores.
    polarityPositive = polarityScores(seedsPositive,vocabulary,wordGraph,depth);
    polarityNegative = polarityScores(seedsNegative,vocabulary,wordGraph,depth);
    
    % Account for difference in overall mass of positive and negative flow
    % in the graph.
    b = sum(polarityPositive) / sum(polarityNegative);
        
    % Calculate new sentiment scores.
    sentimentScoresNew = polarityPositive - b * polarityNegative;
    sentimentScoresNew = normalize(sentimentScoresNew,'range',[-1,1]);
    
    % Add scores to sum.
    sentimentScores = sentimentScores + sentimentScoresNew;
end

Нормализуйте счета настроений по количеству итераций.

sentimentScores = sentimentScores / maxPathLength;

Составьте таблицу, содержащую словарь и соответствующие счета настроений.

tbl = table;
tbl.Token = vocabulary';
tbl.SentimentScore = sentimentScores';

Чтобы удалить лексемы с нейтральными настроениями из лексикона, удалите лексемы с счетом настроения, которые имеют абсолютное значение меньше порога 0,1.

thr = 0.1;
idx = abs(tbl.SentimentScore) < thr;
tbl(idx,:) = [];

Отсортируйте строки таблицы по убыванию балла настроений и просмотрите первые несколько строк.

tbl = sortrows(tbl,'SentimentScore','descend');
head(tbl)
ans=8×2 table
         Token         SentimentScore
    _______________    ______________

    "opportunities"       0.95633    
    "innovative"          0.89635    
    "success"             0.84362    
    "focused"             0.83768    
    "strong"              0.81042    
    "capabilities"        0.79174    
    "innovation"          0.77698    
    "improved"            0.77176    

Можно использовать эту таблицу в качестве пользовательского лексикона настроений для vaderSentimentScores функция.

Визуализируйте лексикон настроения в облаках слов. Отобразите лексемы с положительным счетом в одном облаке слов и лексемы с отрицательными счетами в другом. Отобразите слова с размерами, заданными абсолютным значением их соответствующего счета настроения.

figure
subplot(1,2,1);
idx = tbl.SentimentScore > 0;
tblPositive = tbl(idx,:);
wordcloud(tblPositive,'Token','SentimentScore')
title('Positive Words')

subplot(1,2,2);
idx = tbl.SentimentScore < 0;
tblNegative = tbl(idx,:);
tblNegative.SentimentScore = abs(tblNegative.SentimentScore);
wordcloud(tblNegative,'Token','SentimentScore')
title('Negative Words')

Экспорт таблицы в файл CSV.

filename = "financeSentimentLexicon.csv";
writetable(tbl,filename)

Анализ настроения в тексте

Чтобы проанализировать настроения для ранее не просматривавшихся текстовых данных, предварительно обработайте текст с помощью тех же шагов предварительной обработки и используйте vaderSentimentScores функция.

Создайте строковые массивы, содержащий текстовые данные, и предварительно обработайте его с помощью preprocessText функция.

textDataNew = [
    "This innovative company is continually showing strong growth."
    "This other company is accused of misleading consumers."];
documentsNew = preprocessText(textDataNew);

Оцените настроение с помощью vaderSentimentScores функция. Задайте лексикон настроения, созданный в этом примере с помощью 'SentimentLexicon' опция.

compoundScores = vaderSentimentScores(documentsNew,'SentimentLexicon',tbl)
compoundScores = 2×1

    0.4360
   -0.1112

Положительные и отрицательные счета указывают на положительные и отрицательные настроения, соответственно. Величина значения соответствует силе настроения.

Вспомогательные функции

Функция предварительной обработки текста

The preprocessText функция выполняет следующие шаги:

  • Удалите все URL-адреса.

  • Токенизируйте текст.

  • Удалите лексемы, содержащие цифры.

  • Преобразуйте текст в нижний регистр.

  • Удалите любые слова с двумя или меньшим количеством символов.

  • Удалите все стоповые слова.

function documents = preprocessText(textData)

% Erase URLS.
textData = eraseURLs(textData);

% Tokenize.
documents = tokenizedDocument(textData);

% Remove tokens containing digits.
pat = textBoundary + wildcardPattern + digitsPattern + wildcardPattern + textBoundary;
documents = replace(documents,pat,"");

% Convert to lowercase.
documents = lower(documents);

% Remove short words.
documents = removeShortWords(documents,2);

% Remove stop words.
documents = removeStopWords(documents);

end

Функция счетов полярности

The polarityScores функция возвращает вектор полярности счетов заданный набор seed слов, словаря, графика и заданной глубины. Функция вычисляет сумму по максимальному взвешенному пути от каждого начального слова к каждому узлу в словаре. Высокий счет полярности указывает фразы, соединенные с несколькими начальными словами как через короткие, так и сильно взвешенные пути.

Функция выполняет следующие шаги:

  • Инициализируйте счета семян с таковые и в противном случае нули.

  • Цикл над семенами. Для каждого seed итеративно пройдите график на разных уровнях глубины. Для первой итерации установите пространство поиска для ближайших соседей seed.

  • Для каждого уровня глубины закольцовывайте узлы в пространстве поиска и идентифицируйте его соседи в графике.

  • Закольцовывайте его соседи и обновляйте соответствующие счета. Обновленный счет является максимальным значением текущего счета для начального и соседнего, и счетом для начального и поискового узла, взвешенным соответствующим ребром графика.

  • В конце поиска уровня глубины присоедините соседи к пространству поиска. Это увеличивает глубину поиска следующей итерации.

Полярность выхода является суммой счетов, соединенной с семенами входа.

function polarity = polarityScores(seeds,vocabulary,wordGraph,depth)

% Remove seeds missing from vocabulary.
idx = ~ismember(seeds,vocabulary);
seeds(idx) = [];

% Initialize scores.
vocabularySize = numel(vocabulary);
scores = zeros(vocabularySize);
idx = ismember(vocabulary,seeds);
scores(idx,idx) = eye(numel(seeds));

% Loop over seeds.
for i = 1:numel(seeds)
    
    % Initialize search space.
    seed = seeds(i);
    idxSeed = vocabulary == seed;
    searchSpace = find(idxSeed);
    
    % Search at different depths.
    for d = 1:depth
    
        % Loop over nodes in search space.
        numNodes = numel(searchSpace);
        
        for k = 1:numNodes
            
            idxNew = searchSpace(k);
            
            % Find neighbors and weights.
            nbrs = neighbors(wordGraph,idxNew);
            idxWeights = findedge(wordGraph,idxNew,nbrs);
            weights = wordGraph.Edges.Weight(idxWeights);
            
            % Loop over neighbors.
            for j = 1:numel(nbrs)
                
                % Calculate scores.
                score = scores(idxSeed,nbrs(j));
                scoreNew = scores(idxSeed,idxNew);
                
                % Update score.
                scores(idxSeed,nbrs(j)) = max(score,scoreNew*weights(j));
            end
            
            % Appended nodes to search space for next depth iteration.
            searchSpace = [searchSpace nbrs'];
        end
    end
end

% Find seeds in vocabulary.
[~,idx] = ismember(seeds,vocabulary);

% Sum scores connected to seeds.
polarity = sum(scores(idx,:));

end

Библиография

  1. Великович, Ленид. «Жизнеспособность Web-производных лексиконов полярности». В трудах Ежегодной конференции североамериканского отделения Ассоциации вычислительной лингвистики, 2010 год, стр. 777-785. 2010.

  2. Доступ к данным EDGAR. https://www.sec.gov/edgar/searchedgar/accessing-edgar-data.htm