Çfarë dhe pse

Që kur doli ChatGPT, më duhet të them se qëndrimi në krye të tendencave aktuale të ML dhe praktikave më të mira ka filluar të bëhet paksa punë e përditshme. Ka disa iniciativa fantastike "me burim të hapur" dhe "me pagesë" për të gjurmuar kërkimet në zhvillim, por për mendimin tim, ato nuk mbulojnë aspektet e "inxhinierisë së aplikuar" që më interesojnë më shumë. Për këtë arsye, rregullimi im për këtë ka qenë ndjekja e një sërë temash dhe figurash të respektuara në terren nëpër burime të ndryshme të mediave sociale dhe përdorimi i opinioneve të tyre si pikënisje për të informuar veten. Kjo përgjithësisht funksionon shkëlqyeshëm, pasi shumica e këtyre burimeve paraqesin një lloj funksioni të shënimeve. Natyrisht, faqerojtësit vendosen midis platformave dhe konsolidimi i përmbajtjes midis tyre është pak i lodhshëm, kështu që mendova të përpiqesha t'i konsolidoja këto pjesë të ndryshme të përmbajtjes në një vend. Ky post detajon se si unë:

  • Merrni të dhëna nga Twitter, Reddit, GitHub dhe LinkedIn
  • Aplikoni dhe vlerësoni një klasifikim të rëndësisë së bazuar në LLM për secilën pjesë të përmbajtjes, duke përdorur OpenAI API dhe Argilla
  • Ruani rezultatet në një bazë të dhënash nocionesh

Përmbajtja burimore

Me kalimin e kohës kam zbuluar se secila prej këtyre platformave gërvisht kruajtje të ndryshme, me përshtypjet e mia të përgjithshme për secilën qenie:

  • Twitter. Akademikët që nxisin kërkime të reja, zhvillues që reklamojnë softuer të rinj të mbyllur/me burim të hapur, ndërtues dhe ndërtues. Meme të dendura. Disa reklama në varësi të kujt ndjek.
  • Reddit. Si më sipër, por zakonisht formojnë opinione/pjesë më të gjata. Definitivisht jo aq aktual sa Twitter, dhe shpesh gjej se ka mungesë diskutimi rreth metodave SOTA, duke gabuar në anën e temave të stilit të pyetjeve/përgjigjeve fillestare. Përgjithësisht ende i dobishëm, plus kaloj mjaft kohë këtu për interesa/arsye të tjera.
  • Github. E lidhur rreptësisht me mjetet dhe kodin (duh). Unë përdor kryesisht GitHub për të luajtur projekte interesante dhe depo. Kohët e fundit (2020? Koha e marrjes së MSFT?) Kam vënë re se Github ka krijuar funksione të ngjashme me burimet, i cili i lejon përdoruesit të ndjekin aktivitetin e njëri-tjetrit, në këtë pikë ai bëhet dukshëm më i dobishëm për këtë stil të punës zbuluese.
  • Linkedin. Softuer dhe zhvillime të korporatës/me pagesë. Pak bleh dhe nëse kaloni shumë kohë këtu do t'ju kalbet truri. Duke thënë se më duket e dobishme për njoftimet kryesore nga kompanitë e teknologjisë "mainstream". Dhe njësoj si Reddit, unë jam tashmë këtu vetëm për të parë se çfarë po bëjnë kolegët/miqtë, kështu që mund të shënoj edhe gjëra të dobishme për më vonë.

Gëlltitja e burimit

  • Zapier. Automatizimi-si-shërbim dukej si një pikënisje e mirë për prototip, kështu që hetova "integrimet e Zapier" dhe u zhgënjeva kur mësova se (në kohën e shkrimit) vetëm Twitter dhe Notion janë të mbështetura, me një kapak mjaft të rreptë në "Zaps" përpara se të kërkohet pagesa. Bummer.
  • Twitter. Unë konfigurova një "aplikacion" të Twitter-it, i cili gjeneroi një çelës, çelës sekret, kodin e hyrjes dhe sekretin e kodit të hyrjes. Kam përdorur bibliotekën e klientëve tweepy për të rikuperuar të gjitha cicërimat e pëlqyera të lidhura me profilin tim. Interesante, aplikacioni im fillestar u refuzua rastësisht në gjysmë të rrugës së zhvillimit, që ishte pothuajse në të njëjtën kohë që "Elon po ngatërronte modelin e biznesit të twitter API".
  • Reddit. Në mënyrë të ngjashme, konfigurova një aplikacion Reddit, i cili gjeneroi një ID klienti dhe sekret klienti, i cili përdoret së bashku me emrin tim të përdoruesit, fjalëkalimin dhe një varg agjenti përdoruesi në Reddit. Kam përdorur bibliotekën praw për të tërhequr të gjitha komentet/postimet e mia të ruajtura.
  • Github. Këtu, më duhej vetëm të krijoja një "token personal aksesi". Kam përdorur kërkesa të papërpunuara për të hyrë në GitHub API në vend të një klienti të dedikuar Python.

Linkedin

Siç rezulton, nxjerrja e postimeve të mia të reaguara nga llogaria ime në LinkedIn ishte jashtëzakonisht e vështirë. "API" zyrtar i LinkedIn përgjithësisht është i orientuar drejt përdorimit të biznesit (boo), ndërsa "dokumentet e konsumatorit" janë rrethrrotullues dhe përgjithësisht të padobishëm.

  • linkedin-api. Ekziston një bibliotekë jozyrtare Python, e cila është një mënyrë e shkëlqyeshme për të përmbushur rastet standarde të përdorimit si rikthimi i profilit, dërgimi i mesazheve, fshirja e lidhjes etj., por ende i mungonte aksesi në reagimet/pëlqimet lidhur me profilin tim. Vendosa të hetoj disa opsione gërvishtjeje.
  • Seleni. Në mënyrë të veçantë, lidhjet e Python për Seleniumin. Unë e pashë Selenium të dobishëm si pikënisje, por gjeta se shfletuesi kokëfortë ishte jashtëzakonisht i ngadalshëm në krahasim me shfletuesin tim të rregullt, dhe më e rëndësishmja, i ngadalshëm në krahasim me drejtuesit që dërgohen me Playwright.
  • **Playwright.** Një alternativë ndaj Seleniumit, që paraqet në mënyrë të ngjashme opsionet e shfletimit pa kokë/të plotë të destinuara për testimin/zhvillimin e pjesës së përparme. E gjeta se "Playwright" ishte opsioni më modern dhe më aktiv për të dy.

Kodi i mëposhtëm tërheq kredencialet e mia të LinkedIn, hyn në profilin tim, lokalizon postimet e mia të reaguara dhe më pas përsëritet dhe analizon artikujt e mi të pëlqyer. Për secilin artikull të pëlqyer, ne më pas (në mënyrë të bezdisshme), duhet të "klikojmë" butonin "kopjo në clipboard" të lidhur me secilin artikull për të marrë URL-në.

def parse_post(update_container):
    if text := get_post_description(update_container):
        return {
            "user": get_post_author(update_container),
            "url": get_post_url(update_container),
            "date_created": datetime.now(timezone.utc).strftime(
                "%Y-%m-%dT%H:%M:%S.%fZ"
            ),
            "type": "post",  # TODO: post taxonomy?
            "source_system": "linkedin",
            "text": get_post_description(update_container),
            "meta": {},
        }
    logger.warning("Unable to retrieve text for post")
    return None

def get_liked_posts():
    # Start Playwright with a headless Chromium browser
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=HEADLESS)
        page = browser.new_page()
        # Login to LinkedIn
        logger.info("Logging into LinkedIn..")
        page.goto("<https://www.linkedin.com/login>")
        page.fill("#username", os.environ["LINKEDIN_EMAIL"])
        page.fill("#password", os.environ["LINKEDIN_PASSWORD"])
        page.press("#password", "Enter")
        page.wait_for_selector("input.search-global-typeahead__input")
        # Go to reaction posts
        logger.info("Navigating to reaction page..")
        page.goto("<https://www.linkedin.com/in/samhardyhey/recent-activity/reactions/>")
        # Prevent against weird page failures?
        page.reload()
        # Scroll down a bit to load more posts
        page.evaluate("window.scrollBy(0, 10000)")
        time.sleep(N_WAIT_TIME)
        # Wait for any DM dialog to appear, then close it
        button_selector = "button.msg-overlay-bubble-header__control"
        button_elements = page.query_selector_all(button_selector)
        dm_button = button_elements[1]
        dm_button.click()
        # Get the update containers for each liked post
        logger.info("Retrieving update containers..")
        update_containers = page.query_selector_all(
            ".profile-creator-shared-feed-update__container"
        )
        update_containers = [c for c in update_containers if len(c.text_content()) > 2]
        # Parse each post and collect the results
        posts = []
        logger.info("Parsing update containers..")
        for update_container in update_containers:
            parsed_post = parse_post(update_container)
            posts.append(parsed_post)
        logger.info(f"LinkedIn: found {len(posts)} saved posts")
        # Clean up the browser
        browser.close()
    # Return the parsed posts as a Pandas DataFrame
    posts = [e for e in posts if e]  # filter None
    return pd.DataFrame(posts).drop_duplicates(subset=["user", "text"])

Ruajtja e nocionit

Zgjodha të përdor një bazë të dhënash nocioni si mekanizëm ruajtjeje. Për ta bërë këtë, unë krijova një "integrim të nocionit të brendshëm" dhe inkorporova në mënyrë eksplicite një bazë të dhënash ekzistuese për përdorim brenda këtij integrimi. Në mënyrë të përshtatshme, ekziston një "klient Python" jozyrtar për nocionin, i cili mund të përdoret për të kërkuar bazat e të dhënave, për të shkruar faqe etj. Duke pasur parasysh një rekord postimi të formatuar, ne fillimisht kontrollojmë nëse ekziston brenda bazës së të dhënave të nocioneve, përpara se të formatojmë regjistrimin dhe shkruajmë në nocioni:

def format_notion_database_record(record):
    notion_text_char_limit = 1800  # slightly less than 2000
    meta = json.dumps(record["meta"]) if record["meta"] != {} else "None"
    text = record["text"][:notion_text_char_limit]
    date_created = (
        record["date_created"].to_pydatetime().isoformat()
        if type(record["date_created"]) == pd.Timestamp
        else record["date_created"]
    )
    return {
        "id": {"title": [{"text": {"content": secrets.token_hex(4)}}]},
        "text": {"rich_text": [{"text": {"content": text}}]},
        "user": {"rich_text": [{"text": {"content": record["user"]}}]},
        "url": {"url": record["url"]},
        "date_created": {"date": {"start": date_created}},
        "type": {"select": {"name": record["type"]}},
        "source_system": {"select": {"name": record["source_system"]}},
        "meta": {"rich_text": [{"text": {"content": meta}}]},
        "is_tech_related": {"select": {"name": record["is_tech_related"]}},
    }

@retry(
    wait=wait_random_exponential(min=1, max=60),
    stop=stop_after_attempt(NOTION_MAX_RETRY),
)
def write_notion_page(new_database_record, database_id):
    res = notion_client.pages.create(
        parent={"database_id": database_id},
        properties=new_database_record,
    )

Më dukej se nocioni API ishte .. interesant për të thënë të paktën. Shumë fole, shumë vargje, kontrolle të çuditshme të integritetit për disa gjëra, por jo për të tjera, etj. Unë gjithashtu i kam mbështjellë të gjitha thirrjet me një mekanizëm të riprovës duke përdorur bibliotekën këmbëngulje, që mendova se ishte mjaft e rregullt. Gjithsesi, skripti kryesor i gëlltitjes duket kështu:

if __name__ == "__main__":
    # 1. retrieve
    reddit_posts = get_saved_posts()
    twitter_posts = get_liked_tweets()
    github_repos = get_starred_repos()
    linkedin_posts = get_liked_posts()

    # 2. format
    all_records = pd.concat(
        [reddit_posts, twitter_posts, github_repos, linkedin_posts]
    ).to_dict(orient="records")
    logger.info(f"Found {len(all_records)} records to write to Notion")
    # 3. write
    for record in all_records:
        if find_record_by_property("text", record["text"]):
            logger.warning(
                f"Record **{record['text'][:200]}** already exists, skipping"
            )
            continue
        else:
            # 3.1 include a relevancy prediction
            time.sleep(API_THROTTLE)  # ~60 requests a minute
            truncated_input = " ".join(
                record["text"].split(" ")[:TOKEN_TRUNCATION]
            )  # input limits
            record["is_tech_related"] = chain.run({"text": truncated_input}).strip()
            # 3.2 format/write to notion
            new_database_record = format_notion_database_record(record)
            write_notion_page(new_database_record, database_id)

Aty ku ne fillimisht marrim përmbajtje nëpër platforma, formatoni secilën pjesë të përmbajtjes në regjistrime dhe më pas shkruajmë çdo regjistrim në bazën e të dhënave të nocioneve. Ju gjithashtu ndoshta keni vënë re se në 3.1 ne po llogarisim gjithashtu një parashikim të rëndësisë, më shumë për këtë më poshtë.

Klasifikimi

Në formën e tij aktuale, skenari na lejon të shkruajmë përmbajtje të shënuar në nocionin, gjë që është e mrekullueshme! Por tani kemi një problem të ri; nuk ka asnjë dallim midis përmbajtjes së lidhur me ML dhe gjithçka tjetër që më ka pëlqyer. Pra, ne kemi nevojë për një mënyrë për të klasifikuar përmbajtjen hyrëse, në mënyrë që ta filtrojmë më vonë. Në këtë pikë unë ndoshta do të arrija për një mësim tradicional të mbikqyrur për të klasifikuar rëndësinë e regjistrimeve hyrëse, por pashë disa përdorime interesante të Open AI API që lejojnë përdorimin e klasifikimit me goditje zero, kështu që ne do ta provojmë atë.

  • Langchain. Me fjalët e tyre, një kornizë për zhvillimin e aplikacioneve të mundësuar nga modelet gjuhësore. Për mendimin tim, vlera që ofron është se në një kohë kur ka shumë inerci dhe pasiguri në hapësirë ​​se si duhet të ndërtohen aplikacionet e bazuara në LLM, Langchain ofron një grup të përshtatshëm (nëse ndonjëherë të fryrë) abstraksionesh dhe opinionesh. që mund të përdoren për të përshpejtuar procesin e ndërtimit. Kjo, dhe ata duket se kanë kapur avantazhin e lëvizjes së parë, janë me burim të hapur dhe kanë mbështetje të fortë nga komuniteti/zhvilluesi. Pra, thelbi i asaj që duam të bëjmë është të përcaktojmë një kërkesë (e cila është thjesht një mënyrë fantastike për të ndërthurur një varg), që përfshin tekstin e përmbajtjes që ne po kërkojmë të klasifikojmë. Në fakt, freskuese e drejtpërdrejtë dhe disi e mërzitshme:
_PROMPT_TEMPLATE = """You are subject matter expert specializing in computer science, programming and machine learning technologies.
You are to classify whether the following text:
{text}
Is likely to relate to computer science, programming or machine learning or not. Please provide one of two answers: tech, not_tech.
"""

prompt = PromptTemplate(input_variables=["text"], template=_PROMPT_TEMPLATE)
llm = OpenAI(
    model_name="text-davinci-003",
    temperature=0,
    openai_api_key=os.environ["OPEN_API_KEY"],
)
chain = LLMChain(llm=llm, prompt=prompt)

Të cilin më pas mund ta thërrasim në skriptin tonë kryesor si më sipër.

Vlerësimi. Në rregull, shoku, kështu që po më thua se mund të kapërcej procesin tradicional dhe të lodhshëm të të mësuarit të mbikëqyrur me një thirrje API dhe të marr ende rezultate konkurruese? Ndoshta! Ne duhet ta testojmë këtë. Kështu që unë ngrita një shembull të shënimit "argilla" në makinën time lokale. E bëra këtë me konfigurimin standard docker-compose, duke qenë i ndërgjegjshëm që të shkulja arg të platformës për të synuar AMD64 (M1 Mac) dhe për të shtuar vëllime të dhënash për të vazhduar grupet e të dhënave ndërmjet përdorimit. Kam shkruar disa skripta të tjera për të tërhequr të gjithë bazën e të dhënave të nocionit si një kornizë të dhënash (juck):

@retry(
    wait=wait_random_exponential(min=1, max=NOTION_MAX_RETRY_TIME),
    stop=stop_after_attempt(NOTION_MAX_RETRY),
)
def notion_db_to_df(notion_client, database_id):
    # Create an empty list to hold all pages
    data = []

    # Initialize start_cursor as None to get the first page of results
    start_cursor = None
    while True:
        time.sleep(0.2)
        # Get a page of results
        response = notion_client.databases.query(database_id, start_cursor=start_cursor)
        results = response.get("results")
        # Convert the pages to records and add them to data
        for page in results:
            record = {
                prop_name: get_property_value(page, prop_name)
                for prop_name in page["properties"].keys()
            }
            data.append(record)
        if next_cursor := response.get("next_cursor"):
            # Otherwise, set 'start_cursor' to 'next_cursor' to get the next page of results in the next iteration
            start_cursor = next_cursor
        else:
            break
    # Convert the data to a dataframe and return it
    return pd.DataFrame(data)

def get_property_value(page, property_name):
    # for a notion page/db record
    prop = page["properties"][property_name]
    if prop["type"] == "title":
        return prop["title"][0]["text"]["content"] if prop["title"] else None
    elif prop["type"] == "rich_text":
        return prop["rich_text"][0]["text"]["content"] if prop["rich_text"] else None
    elif prop["type"] == "number":
        return prop["number"]
    elif prop["type"] == "date":
        return prop["date"]["start"] if prop["date"] else None
    elif prop["type"] == "url":
        return prop["url"]
    elif prop["type"] == "email":
        return prop["email"]
    elif prop["type"] == "phone_number":
        return prop["phone_number"]
    elif prop["type"] == "select":
        return prop["select"]["name"] if prop["select"] else None
    elif prop["type"] == "multi_select":
        return (
            [option["name"] for option in prop["multi_select"]]
            if prop["multi_select"]
            else []
        )
    else:
        return None

Dhe ngarkoni këtë kornizë të të dhënave si një grup të dhënash brenda argilla:

def format_metadata(record):
    meta_cols = set(record.keys()) - {"text", "vector"}
    return {k: v for k, v in record.to_dict().items() if k in meta_cols}

def format_classification_record(record):
    record = TextClassificationRecord(
        prediction=[(record.is_tech_related, 1.0)],
        text=record.text,
        multi_label=False,
        metadata=format_metadata(record),
    )
    return record
def log_notion_db_to_argilla():
    classification_records = (
        notion_db_to_df(notion_client, database_id)
        .pipe(lambda x: x[x.text.apply(lambda y: len(y.split(" ")) > 5)])
        .apply(format_classification_record, axis=1)
        .tolist()
    )
    logger.info(f"Logging {len(classification_records)} records to Argilla")
    dataset_rg = DatasetForTextClassification(classification_records)
    log(
        records=dataset_rg,
        name=DATASET_NAME,
        tags={"overview": "Verify zero-shot LLM classifications"},
        background=False,
        verbose=True,
    )

Këtu, unë kam zgjedhur në mënyrë specifike të përdor parashikimet e hapura të lidhjes së teknologjisë së AI si një "atribut para-shënimi" në çdo regjistrim. Kjo në thelb e kthen ushtrimin e shënimit në një ushtrim vërtetimi, duke reduktuar sasinë e futjes së të dhënave që duhet të ndërmarrim. Kështu përfundova duke etiketuar ~ 260 rekorde kryesisht nga Reddit dhe Twitter në ~20 minuta, të cilat më pas i hoqa nga argilla dhe kalova përmes një raporti të klasifikimit të sklearn:

def evaluate_argilla_dataset():
    # after some annotations, load in dataset
    labelled = (
        load(DATASET_NAME)
        .to_pandas()
        .pipe(lambda x: x[~x.annotation.isna()])
        .assign(prediction=lambda x: x.prediction.apply(lambda y: y[0][0]))
    )
    cr = classification_report(
        labelled.annotation, labelled.prediction, output_dict=True
    )
    cr = pd.DataFrame(cr).T
    print(tabulate(cr, headers="keys", tablefmt="psql"))

Raporti i klasifikimit duket si ky:

+--------------+-------------+----------+------------+------------+
|              |   precision |   recall |   f1-score |    support |
|--------------+-------------+----------+------------+------------|
| Not_tech     |    0.894737 | 0.990291 |   0.940092 | 103        |
| Tech         |    0.993464 | 0.926829 |   0.958991 | 164        |
| accuracy     |    0.951311 | 0.951311 |   0.951311 | 0.951311   |
| macro avg    |    0.9441   | 0.95856  |   0.949541 | 267        |
| weighted avg |    0.955378 | 0.951311 |   0.9517   | 267        |
+--------------+-------------+----------+------------+------------+

Nëse jeni të dyshimtë, isha edhe unë. Por unë i kontrollova manualisht rezultatet disa herë dhe mund të konfirmoj se ato janë të sakta. Për të përmbledhur; Unë isha në gjendje të krijoja rezultate pothuajse perfekte të klasifikimit duke përdorur modelin text-davinci-003 të open-AI, në rreth 30 minuta. Yee haw.

Finito

Kështu që tani mund t'i lexoj me lehtësi tweet-et e mia të vogla dhe postimet e mia të makinerive nga komoditeti i idesë! Disa përmirësime/zgjerime të ardhshme:

  • Kontainerizimi + planifikimi. Fillimisht, kisha në mendje një imazh doker që mund të planifikoja rregullisht nëpërmjet një funksioni cloud ose diku që nuk ishte vetëm makina ime lokale, por skrapimi i LinkedIn ka krijuar disa komplikime. Në mënyrë të veçantë, funksioni "kopjo në kujtesën e fragmenteve" kërkon qasje në kujtesën e kujtesës së një makine pritës për ta kopjuar gjithashtu. Për shumicën e imazheve të bazës docker, kjo kërkon që të instalohet një server X11 për të lehtësuar protokollin X11 që lejon aplikacionet të kopjojnë në kujtesën e fragmenteve dhe gjithashtu krijon një rrezik shtesë sigurie, i cili është shumë i bezdisshëm kur më duhet vetëm ta ekzekutoj këtë skript në mënyrë periodike, dhe në mënyrë ad hoc. Kështu që nuk e bëra këtë, por mund ta bëja.
  • Modeli i mbikëqyrur. Gjëja tjetër që gjeta ishte se (mjaft e kuptueshme), hapni API-të e tyre "kufizojnë normën" e AI mbi një bazë 60 kërkesa/minutë ose kufirin e shenjës X max (që ndryshon sipas modelit), me kufijtë e tarifave që zbatohen sa herë që një nga këto kushte plotësohet për herë të parë. Kjo është mjaft e vështirë, pasi çdo rekord që po përpiqem të shkruaj në nocion kërkon thirrjen/klasifikimin e vet të veçantë API. Unë mendoj se "ky postim në blog" nga Mathew Honnibal përmban disa këshilla të arsyeshme që janë të dobishme këtu; me të cilat LLM-të mund të jenë të dobishme si pikënisje për prototipimin e ideve, por nëse detyra mund të qartësohet/përsëritet me mësimin tradicional të mbikëqyrur, atëherë këto modele tradicionale përfitojnë nga shpejtësia (RE: kufizimi i shkallës), kontrolli dhe shtrirja. Të gjitha gjërat e mira duam që softueri ynë të jetë.

Gjithsesi, mund ta gjeni repon "këtu" nëse doni të gërvishtni më shumë.

Arti i banerëve u zhvillua me "përhapje të qëndrueshme". Detaje teknike të nivelit të lartë të zhvilluara në bashkëpunim me GPT-4.