Rozšíření matice ve velkém měřítku pomocí pyspark (nebo - jak sladit dva velké datové sady názvů společností)

Spark a pyspark mají skvělou podporu pro spolehlivou distribuci a paralelizaci programů a podporu mnoha základních algebraických operací a algoritmů strojového učení.

V tomto příspěvku popisujeme motivaci a prostředky provádění párování jmen podle jmen dvou velkých datových souborů názvů společností pomocí Spark.

První motivace

Naším cílem je sladit dvě velké sady názvů společností. Hledáme dva dlouhé seznamy názvů společností, seznam A a seznam B a snažíme se spojit společnosti z A se společnostmi z B.

Obvykle bychom měli něco takového:

Seznam A | Seznam B
---------------------
GOOGLE INC Google
MEDIUM.COM | Střední Inc
Amazonské laboratoře | Amazonka
Google, vč
Yahoo |
             | Microsoft

V tomto příkladu je naším cílem sladit GOOGLE INC. A Google, inc (ze seznamu A) s Google (ze seznamu B); a přizpůsobit MEDIUM.COM střední Inc; a Amazonské laboratoře do Amazonu atd.…

Při pohledu na tento jednoduchý příklad vyniká několik věcí:

  • Je možné, aby více společností z A odpovídalo jedné společnosti z B; je to vztah mezi dvěma.
  • Názvy společností jsou ve většině případů pro člověka snadno srovnatelné (např. Amazonské laboratoře k Amazonce), ale nejsou pro počítače tak snadné porovnávat (jak počítač ví, že „laboratoře“ jsou v tomto případě zanedbatelné a že „Amazonka“ je jen zkratka pro „Amazon labs“?)
  • Ne všechny společnosti z A mají zápasy v B a ne všechny společnosti z B jsou přiřazeny od A. V našem příkladu Yahoo ze seznamu A není uzavřeno s žádnou jinou společností na B a Microsoft z B není uzavřeno s žádnou společností na A .
  • Jakýkoli prvek ze seznamu A by měl mít nanejvýš jeden zápas od B. Opak je pravdou, jak je uvedeno, mnoho společností z A by mohlo být spojeno do jediné společnosti na B.

První pokus - triviální zápas

Dobře, nejdřív jsme si mysleli, že vyzkoušíme nejjednodušší a triviální řešení, abychom zjistili, jak dobře to funguje, ne-li nic jiného, ​​alespoň proto, abychom vytvořili základní linii pro budoucí pokusy. Nejjednodušší je udělat test řetězcových rovnic bez rozlišování malých a velkých písmen. Jednoduše přiřaďte řetězce od A k řetězcům od B.

Přesnost a odvolání

Je třeba se podívat na dvě relevantní opatření: Přesnost a Odvolání. Přesnost je „kolik chyb jsme (ne) udělali“, jinými slovy - vzhledem ke všem zápasům, kolik z nich bylo skutečně správných zápasů - přesné zápasy. Takže přesnost se týká falešných pozitiv.

Připomeňme si naopak: „kolik zápasů mělo být nalezeno, ale zmeškáno“. Takže vzpomínka je o falešných negativech.

První triviální pokus byl, jak se očekávalo, vysoká přesnost, ale nízká zpětná vazba. Podíváme-li se na krátký seznam příkladných společností, bude to odpovídat nulovým prvkům od A do B. To je velmi špatné vzpomínky, ale 100% přesnost ;-). Ve scénáři v reálném světě by to samozřejmě odpovídalo o něco více než nula, ale je snadné vidět, že vzhledem k mnoha malým možným změnám v názvech společností zůstane stahování nízké.

Jednoduchým vylepšením by bylo odstranění stopových slov.

Zastavte slova

Co jsou stop-slova? Stop-slova jsou slova, která se odstraní před zpracováním různých algoritmů NLP, protože nepřidávají informace, obvykle pouze přidávají šum. V angličtině jsou běžná slova typu „of“, „for“ „if“ atd., Jedná se o velmi běžná slova, která se velmi často používají, ale pro mnoho algoritmů NLP a IR pak informace nepřidávají. Dělají správné syntaktické věty a v mnoha případech ovlivňují sémantiku, ale na úrovni mnoha procesorů NLP, které se nedívají na skutečnou syntaxi, nemají význam.

V našem případě nejsou slova „if“, „of“ nebo „for“ typická v angličtině, ale jsou to „inc“ a „llc“ z rozšíření společnosti. Naším jednoduchým vylepšením je tedy pouze odstranit všechna tato rozšíření společnosti a zkusit jednoduchou řetězcovou rovnici znovu.

To skutečně pomohlo a jak vidíte v našem příkladu, pomohlo to přiřadit „Google inc.“ K „Google“ a jednoduchým odstraněním interpunkce a správným tokenizací také „Google, inc“ a „Google“. Ale to stále neodpovídá „Amazon labs“ a „Amazon“ b / c „labs“ není stop-slovo v tom smyslu, že se nejedná o běžné rozšíření společnosti. Ukázalo se, že „Amazonské laboratoře“ nejsou jen náhodným příkladem. Mnoho názvů společností má tyto variace v jejich jménech, které se projevují v jednom datovém souboru, ale v jiných souborech dat se neprojevují. Závěr: Musíme najít způsob, jak se „podívat za to“, ignorovat „laboratoře“ v „amazonských laboratořích“.

Setkáme se s vědou.

Věda

Zde se zabýváme problémem sladění N dokumentů ze seznamu A do M dokumentů ze seznamu B, ve vztahu mezi dvěma. Náš porovnávací algoritmus však musí být „chytrý“ v tom smyslu, že musí být schopen rozlišovat mezi „důležitými slovy“ a „nepodstatnými slovy“. Musíme najít způsob, jak počítači sdělit, že „laboratoře“ v „amazonských laboratořích“ jsou zanedbatelné, ale „amazon“ je skutečně významný. Také bychom triviálně tokenizovali jména do menších tokenů rozdělením mezerami, interpunkcemi atd., Aby se „medium.com“ rozpadlo na „medium“ a „com“.

Věda na záchranu!

TF-IDF

Za tímto účelem používáme společné schéma v teorii získávání informací nazvané TF-IDF. TF-IDF je zkratka pro Term Frequency - Invertted Document Frequency. Termín Frekvence jednoduše znamená „kolikrát se toto slovo v tomto dokumentu objeví“ (naše dokumenty jsou pouze názvy společností, takže jsou velmi krátkými „dokumenty“). Takže v případě „amazonových laboratoří“ máme v dokumentu „amazon“ a „laboratoře“ pouze dvě slova a jejich frekvence je jednoduše 1 a 1. (Pokud se mimochodem název společnosti právě stal „amazonové laboratoře“ amazon “pak by počet byl 2 pro„ amazon “a 1 pro„ laboratoře “.) O tom je TF, docela jednoduché: Spočítat frekvenci termínů v dokumentu.

Frekvence převráceného dokumentu je skutečná dohoda. Invertovaná frekvence dokumentů se dívá na všechny „dokumenty“ (aka corpus, všechna jména společností) a testuje, jak často se slovo „laboratoře“ objevuje ve všech z nich. Pokud se slovo „amazon“ objeví pouze v jediném dokumentu, znamená to, že „amazon“ je významné slovo, ale pokud se slovo „labs“ objeví v mnoha jiných dokumentech (např. Mnoho společností používá slovo „labs“ jako součást svých dokumentů jméno) pak to znamená, že slovo „labs“ je zanedbatelné. IDF je právě to - v kolika dokumentech se slovo objevuje.

TF-IDF je TF termínu děleno IDF daného termínu. Poskytuje dobré měření toho, jak důležitá nebo jak důležitá jsou slova v kontextu konkrétních dokumentů.

Je snadné vypočítat matici TF-IDF pro sadu dokumentů. Existují hotové knihovny, které to dělají, a použili jsme k tomu implementaci scikit-learn.

Matice TF-IDF je dvourozměrná matice, ve které řádky představují dokumenty (v našem případě - názvy společností) a sloupce představují jedinečné tokeny (nebo slova). Pokud bychom chtěli vytvořit matici TF-IDF našeho malého korpusu ze seznamu A, vypadalo by to takto (po odstranění stop slov, interpunkce a snížení obsahu všeho):

           | google | střední | com | yahoo | amazon | laboratoře
-------------------------------------------------- ---------
GOOGLE INC 1 0 0 0 0 0
MEDIUM.COM | 0,77 0,63 0 0 0
Amazonské laboratoře | 0 0 0 0 .7 .7
Google, vč 1 0 0 0 0 0
Yahoo | 0 0 0 1 0 0
com | 0 0 1 0 0 0

Zde je kód:

ze sklearn.feature_extraction.text importu TfidfVectorizer
matrix = vectorizer.fit_transform (['GOOGLE', 'MEDIUM.COM', 'Amazon labs', 'Google', 'Yahoo', 'com']))

Matice, která byla vytvořena, je NxM, kde N = počet společností a M = počet jedinečných tokenů.

Všimli jste si, že jsme přidali další (vytvořenou) společnost s názvem „com“. Udělali jsme to, abychom demonstrovali důležitou vlastnost TF-IDF. Používáme TF-IDF k rozlišování mezi významnými a nevýznamnými tokeny v dokumentech. Významným tokenem v dokumentu je token, který se v dokumentu nejen objevuje často, ale je také relativně vzácný v celém korpusu. Pokud se termín objevuje v korpusu mnohokrát, stává se pro tento konkrétní dokument méně významný. Přidali jsme vytvořenou společnost „com“ tak, aby se „Medium“ v „Medium.com“ stalo významnějším. (Všimli byste si, že váha „Střední“ je 0,77, zatímco váha „com“ je 0,63, a to kvůli vzhledu „com“ v jiném dokumentu, a proto je IDF nižší).

V reálném světě byste samozřejmě měli desítky nebo stovky názvů společností s tokenem „com“ nebo „labs“, takže v názvu Medium.com byste viděli podstatný rozdíl mezi „Medium“ a „com“.

Kosine podobnost

Dalším krokem po výpočtu matice TF-IDF pro obě strany (oba seznamy A a B společností) je znásobení matic.

Mnohočetné matice poskytují zajímavé měřítko zvané Kosinova podobnost. Kosinová podobnost je jednoduché měření podobnosti, které se pohybuje mezi 0 a 1. Hodnota 1 označuje identické prvky a hodnota 0 označuje zcela odlišné prvky (stejně jako funkce cosine trig). Násobení matic poskytuje kosinovou podobnost mezi každým prvkem v seznamu A a každým prvkem v seznamu B. Ve skutečnosti A vynásobíme A B.T (B.transpose) tak, aby se rozměry přizpůsobily. Zajímavostí o kosinové podobnosti mezi maticemi TF-IDF je to, že výsledkem je matice podobností mezi každým prvkem v A a každým prvkem v B, přičemž se bere v úvahu význam tokenů v názvech. Výsledek> .8 obvykle znamená platnou shodu.

Naštěstí balíček python sklearn poskytuje jednoduchou funkci cosine_similarity, která přijímá dvě matice a vede ke kosinové podobnosti těchto dvou. Zde je ukázkový kód:

ze sklearn.feature_extraction.text importu TfidfVectorizer
ze sklearn.metrics.pairwise import cosine_similarity
a = vectorizer.fit_transform (['aa', 'bb', 'aa bb', 'aa aa bb'])
b = vectorizer.fit_transform (['aa', 'bb'])
cosimilarities = cosine_similarity (a, b)

Výsledkem je matice podobností mezi každým prvkem v A a každým prvkem v b.

           aa | bb
----------------------
aa | 1 | 0
bb | 0 | 1
aa bb | .7 | .7
aa aa bb | .89 | 0,44

Jak se očekávalo, slovo „aa“ z a je velmi podobné slovu „aa“ z b (poznámka 1). Můžete také vidět, že „aa bb“ je stejně podobné jako „aa“ i „bb“, a to také dává smysl. A konečně byste si všimli, že „aa aa bb“ má vyšší podobnost s „aa“ než v případě „bb“. To vše dává smysl.

Shrňme tu vědu. Nejprve vezmeme dva seznamy dokumentů a pro každou sadu vypočítáme matici TF-IDF *. Potom vynásobíme dvě matice, abychom přišli s jejich kosinovou podobností, což je matice, která popisuje podobnost mezi každým dokumentem v A a každým dokumentem v B.

Druhý pokus - násobení matice matice

Poté, co jsme viděli vědu, chceme to vyzkoušet v praxi. Dalším pokusem je načíst všechny skutečné společnosti ze seznamu A a načíst všechny skutečné společnosti ze seznamu B a vynásobit sakra z nich pomocí funkce cosine_similatiry.

Snadnější řekl, než udělal.

V malém měřítku to prostě funguje a funguje to velmi pěkně. Například s několika tisíci společnostmi na každé straně, které by fungovaly. S naším datovým souborem, ve kterém máme v každém seznamu pár stovek tisíc jmen, až několik milionů, se to stává náročným.

Jednoduše spočítat TF-IDF je možné, a to i s tak velkými datovými sadami, na jednom hostiteli (můj notebook běží tak snadno, za pár sekund). Násobení matic je však skutečnou výzvou.

Lokální násobení není v měřítku

Předpokládejme, že v každém seznamu máme 1M (10⁶) jmen. Každá matice by pak byla asi 10 × 10 × (protože počet jedinečných tokenů je podobný počtu společností z důvodu jedinečnosti jmen). Vytvoření matice 10 × 10 × v paměti znamená, že se vznáší 10¹². Python float zabírá 16 bajtů, takže skončíme s 16 * 10¹² bajtů, což se překládá na ~ 4PT (čtyři petabajty) RAM. Můžeme mít v paměti matici 4PT? No, samozřejmě, že ne, alespoň ne na mém ošuntělém notebooku. Ale - je tu trik. Nemusíme to všechno uchovávat v paměti. Mějte na paměti, že ačkoli matice je 1M na 1M, v praxi je většinou vyplněna nulami. Proč bychom tedy měli obtěžovat udržování všech těchto nul v paměti? Místo toho můžeme použít rozptýlenou reprezentaci matice, ve které namísto udržování dvourozměrné matice v paměti pomocí polí polí můžeme namísto toho sledovat pouze souřadnice souřadnic nenulových prvků a předpokládat, že všechny ostatní jsou právě nuly. To je skvělé pro zachování nízké paměti a provádění rychlých operací násobení matic. A ve skutečnosti, to je přesně to, co již obléká. Udržování matic jako řídkých matic znamená, že musíme přidělit pouze kolem 1M floatů (hodnot) plus 2M celých čísel (indexy nenulových prvků), ale ve všech je to asi 24Mbytů, což je celkem snadné.

Ale - znásobení obou matic, i když jsou řídké, by znamenalo přinejmenším 10 ² operací (pokud jsme inteligentní s nulami). Teď je to trochu těžší. A i když numpy (která leží pod sklearn) je v tak rychlé matematice velmi dobrá, i pro numpy je to trochu náročné.

Zkusili jsme to - jednoduše znásobili tyto dvě matice. Funguje to dobře pro dostatečně malou velikost matice, ale u některých čísel (které jsou mnohem menší, než to, co chceme), to začalo selhat a nedostatek paměti. Teď jsme to dokázali vyřešit rozdělením jedné z matic na menší kousky a spuštěním několika nebo násobení jeden po druhém a pak shrnutím všech věcí. Tento druh nám však připomněl, že tento systém, který to dělá, již známe, nazývá se to Spark.

Třetí pokus - násobení jiskrové matice

Jiskra je skvělá pro vysoce paralelní výpočty náročné na paměť a hle a hle, má datový typ BlockMatrix, který implementuje násobnou operaci. Vypadá to, že jsme přesně hledali! Dobře, takže vytváříme matice TF-IDF a převádíme je na Sparkovu BlockMatrix a spouštíme a.multiply (b.transpose ()), což je víceméně to, co cosine_similatiry dělá.

# Pseudo kód...
a_mat = tfidf_vect.fit_transform ([..., ..., ...])
b_mat = tfidf_vect.fit_transform ([..., ..., ...])
a_block_mat = create_block_matrix (a)
b_block_mat_tr = create_block_matrix (b.transpose ())
cosimilarities = a_block_mat.multiply (b_block_mat_tr)

Zdá se, že je to dost snadné, a opravdu je. Ale je tu samozřejmě „ale“… i když je to jednoduché a z matematického hlediska funguje správně - no, to není v měřítku ... Jsme schopni znásobit velké matice, ale ne tak velké, jak bychom chtěli . Snažili jsme se hrát s velikostmi bloků atd. Bohužel, pro dostatečně velké vstupy selhává buď s nedostatkem paměti nebo jen s dlouhými běhy, které nikdy neskončí (hodiny a hodiny).

Co je za problém? Nemůžeš zapálit měřítko?

Jiskra se samozřejmě může škálovat. Ale musíte to používat moudře, hloupě ... Problém s BlockMatrix je v tom, že za účelem implementace operace násobení Spark převádí řídké bloky matice na husté (sub) matice. A ačkoli většina naší matice jsou nuly, jiskra by stále převáděla všechny tyto nuly na hustou reprezentaci, která by buď spotřebovala příliš mnoho paměti, nebo kdybychom zachovali malou velikost bloků, vedlo by to k příliš velkému počtu operací, rozdělení atd. A běhu navždy.

Spark nepodporuje řídké matice, ale tyto matice neimplementují operaci násobení (aka dot) a jedinou distribuovanou maticí, která implementuje operaci násobení od okamžiku zápisu, je BlockMatrix, který, jak bylo uvedeno, převádí řídkou reprezentaci na hustá reprezentace před jejich násobením. Měli bychom si uvědomit, že v komunitě jisker proběhly diskuse o způsobech, jak implementovat distribuované rozmnožování rozptýlené matice, jak bylo uvedeno - v době tohoto psaní to ještě nebylo provedeno.

BlockMatrix.multiply () se nezdařil. Co bude dál?

Čtvrtý pokus - a vítězem je…

Náš čtvrtý a poslední pokus byl úspěšný. Záměrem je míchat a spojovat Spark s numpy. Naše testy ukazují, že numpy jsou schopné znásobit menší matici s větší maticí, takže pokud vezmeme jen malý kus matice A a vynásobíme ji maticí B, bude to fungovat a numpy nebude explodovat. A pokud si vzpomenete na své lekce algebry, lze násobit matice pomocí vektoru po vektoru, takže algebraicky je to správné. Záměrem je rozdělit pouze jednu z matic na menší kousky, a pak nechat každého pracovníka Sparkem, aby se množil na svém kusu, a pak vrátil pouze závěr, např. závěr by mohl být takový, že jméno na A [13] odpovídá jménu na B [21] atd.

Vysílejte a paralelizujte se záchranou

Spark má dvě užitečné funkce: vysílání a paralelizaci. Vysílání jednoduše vysílá stejná data všem zaměstnancům. Používáme vysílání k odeslání matice B všem pracovníkům, aby všichni pracovníci měli úplnou matici B. Paralelní rozdělení dat do oddílů a odeslání každého oddílu jinému pracovníkovi. Paralelizujeme, abychom pracovníkům poslali kousky A, takže každý pracovník má všechny B, ale jen malý kus A.

Zde je obecný přehled:

  1. Vypočítat matice TF-IDF na ovladači.
  2. Paralelizovat matici A; Vysílací matice B
  3. Každý pracovník nyní flatMaps svůj kus práce vynásobením jeho kus matice A celou maticí B. Pokud tedy pracovník pracuje na A [0:99], pak by vynásobil těchto sto řádků a vrátil výsledek, řekněme A [13] ] odpovídá jménu nalezenému v B [21]. Násobení se provádí pomocí numpy.
  4. Řidič by shromažďoval zpět všechny výsledky od různých pracovníků a porovnával indexy (A [13] a B [21]) se skutečnými jmény v původním datovém souboru - a máme hotovo!

Tato metoda funguje velmi dobře a ve skutečnosti, když běžela poprvé, bylo to tak příjemné překvapení, že jsme si mysleli, že to prostě nefunguje (ale ...). Ve srovnání s předchozími metodami, které buď běhaly hodiny (a nedokončily se), nebo došly nebo paměť, byla tato metoda schopna dokončit výpočet v řádu několika minut. Samozřejmě to záleží na velikosti dat a velikosti Sparkova clusteru, ale celkově to fungovalo opravdu dobře.

V tuto chvíli je jediným problémem řidič, který vypočítává matice TF-IDF, a na této frontě stále máme tuny loketního prostoru, protože tento výpočet je pro sklearn stále velmi snadný. (vedlejší poznámka: Spark implementuje také distribuovaný výpočet TF-IDF, ale nemuseli jsme ho používat).

Zde je pseudokód pro ilustraci našeho řešení:

ze sklearn.feature_extraction.text importu TfidfVectorizer
ze sklearn.feature_extraction.text importu CountVectorizer
ze sklearn.metrics.pairwise import cosine_similarity
# tyto by se realisticky získaly ze souborů nebo datových rámců.
a = ['google inc', 'medium.com', ...]
b = ['google', 'microsoft', ...]
stopwords = ['ltd', ...]
vect = CountVectorizer (stop_words = stopwords)
# to lze provést s menší režií paměti pomocí generátoru
vocabulary = vect.fit (a + b) .vocabulary_
tfidf_vect = TfidfVectorizer (stop_words = stopwords,
                             vocabulary = vocabulary)
a_mat = tfidf_vect.fit_transform (a)
b_mat = tfidf_vect.fit_transform (b)
a_mat_para = parallelize_matrix (a_mat, rows_per_chunk = 100)
b_mat_dist = broadcast_matrix (a_mat)
a_mat_para.flatMap (
        lambda submatrix:
        find_matches_in_submatrix (csr_matrix (submatrix [1],
                                             shape = submatrix [2]),
                                   b_mat_dist,
                                   submatrix [0]))
def find_matches_in_submatrix (zdroje, cíle, input_start_index,
                              práh = 0,8):
    cosimilarities = cosine_similarity (zdroje, cíle)
    pro i, cosimilarity v enumerate (cosimilarities):
        cosimilarity = cosimilarity.flatten ()
        # Najděte nejlepší shodu pomocí argsort () [- 1]
        target_index = cosimilarity.argsort () [- 1]
        source_index = vstupy_start_index + i
        podobnost = cosimilarity [target_index]
        if cosimilarity [target_index]> = práh:
            výnos (source_index, target_index, podobnost)
def broadcast_matrix (mat):
    bcast = sc.broadcast ((mat.data, mat.indices, mat.indptr))
    (data, indexy, indptr) = bcast.value
    bcast_mat = csr_matrix ((data, indexy, indptr), shape = mat.shape)
    návrat bcast_mat
defallelize_matrix (scipy_mat, rows_per_chunk = 100):
    [řádky, sloupce] = scipy_mat.shape
    i = 0
    submatrices = []
    zatímco i <řádky:
        current_chunk_size = min (rows_per_chunk, rows - i)
        submat = scipy_mat [i: i + aktuální_chunk_size]
        submatrices.append ((i, (submat.data, submat.indices,
                                submat.indptr),
                            (current_chunk_size, cols)))
        i + = current_chunk_size
    návrat sc.parallelize (submatrices)

Všimli byste si, že po vysílání a paralelizaci matici znovu sestavíme do scipy csr_matrix, z čehož pochází. V zásadě to tedy děláme - serializujeme matice přes drát a poté je znovu sestavíme na druhé straně, na dělníky. Serializace je účinná, protože stačí poslat nenulové prvky řídké matice. Takže pro matici 1M elementů posíláme jen asi 1M floatů spolu s 2M ints, což je určitě v komfortní zóně Spark.

Závěr

Popisujeme metodu pro nalezení podobnosti mezi dvěma seznamy řetězců A a B, které popisují názvy společností. Jako faktor podobnosti jsme použili TF-IDF a kosinovou podobnost.

Dále ukážeme různé pokusy o škálovatelnou implementaci násobení matice pomocí jiskry a výherní metodu, která kombinuje násobení matice matice spolu s možnostmi vysílání a paralelizace jisker.

* Jeden jemný bod, který je třeba zmínit: slovník obou matic musí být stejný. Jinými slovy počet řádků v obou matricích musí být stejný a musí mít přesně stejné pořadí, např. každý řádek představuje termín a pořadí řádků musí být mezi maticí A a maticí B přesně stejné. To se snadno provede nejprve vypočítáním slovní zásoby a teprve poté vypočítáním TF-IDF jako v následujícím příkladu:

ze sklearn.feature_extraction.text importu TfidfVectorizer
ze sklearn.feature_extraction.text importu CountVectorizer
ze sklearn.metrics.pairwise import cosine_similarity
a = ['google inc', 'medium.com']
b = ['google', 'microsoft']
company_name_stopwords = frozenset (['ltd', 'llc', 'inc'])
vect = CountVectorizer (stop_words = company_name_stopwords)
vocabulary = vect.fit (a + b) .vocabulary_
tfidf_vect = TfidfVectorizer (stop_words = company_name_stopwords,
                             vocabulary = vocabulary)
a_mat = tfidf_vect.fit_transform (a)
b_mat = tfidf_vect.fit_transform (b)
cosimilarities = cosine_similarity (a_mat, b_mat)