顯示具有 人工智慧 標籤的文章。 顯示所有文章
顯示具有 人工智慧 標籤的文章。 顯示所有文章

2026年7月17日 星期五

購買 MasterTalk 課程 (補記)

今天查看 LINE 紀錄才想起上周三 (7/8) 請假陪水某與菁菁去大甲鎮瀾宮拜拜, 在自強號上滑手機時看到 MasterTalk 平台的新課程通知 :





有點數折換打 9 折, 實付 3492 元. 

2026年7月14日 星期二

市圖還書 1 本 : 超有料!職場第一實用的AI工作術

此書被預約須還 : 
旗標出版的書內容質感都不錯 (個人評價 No.1), 此書亦然. 此書對於剛要步入職場的社會新鮮人實用度非常高, 從 AI 製做簡報, 影片修圖, 處理郵件+合約, 到廣宣文案生成等等, 涵蓋面足敷剛踏入職場的新手應付工作所需. 

2026年4月15日 星期三

好站 : 工程師下班有約

最近上了幾堂林鼎淵老師的 Vibe coding 內訓課程, 覺得他是實力厚實備課非常認真的老師, 這從等候程式執行的幾分鐘, 他都有準備小知識與開發經驗談來墊檔與分享可見一斑. 他經營的 YT 頻道有非常多實用與最新的 AI 技術分享影片 (尤其是 Vibe coding), 值得記下來有空時學習 : 


2025年12月2日 星期二

GitHub Copilot 初體驗

雖然現在我已使用 AI 協作方式產出程式碼與 debug, 主控權與選擇權仍然在我. 但是最近跟明賢表弟在 LINE 上交流軟體經驗後, 發現我在這部分似乎定格在石器時代了, 我原先對要花錢的 Cursor, GitHub Copilot, 或 Claude Code 等 Vibe coding 工具抱持觀望態度, 認為能透過 ChatGPT 交互問答得到答案又何必要花錢? 直到看過他展示目前的作業模式後, 我的態度與觀點就開始動搖了, 於是決定從 GitHub Copilot 著手來了解 Vibe coding. 

我上周末從母校圖書館借到下面這本書, 依照書中說明啟用 GitHub Copilot 功能 :


登入 GitHub 後, 按右上角人頭, 點選 "Copilot settings" :




在 GitHub Copilot 頁面中按 "Start using Copilot Free" :




出現交談頁面, 輸入提示詞按 Enter 或右方三角形按鈕提交詢問 :




預設模型是 GPT-5 mini, 點下拉式選單可選擇模型, 免費版只能選 GPT-5 mini, GPT 4.1, GPT 4o, 以及 Claude Haiku 4.1 四種模型 :




我用經典的九九乘法表來測試 : 




生成如下程式碼 : 




#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
multiplication_table.py
印出傳統的九九乘法表(1 到 9)
"""

def multiplication_table(n: int = 9) -> None:
    """印出 1..n 的乘法表,每列只顯示到當前被乘數(傳統九九表格式)。"""
    for i in range(1, n + 1):
        for j in range(1, i + 1):
            # i*j 以 2 個字元寬度對齊,方便看齊
            print(f"{j}×{i}={i*j:2}", end="  ")
        print()

if __name__ == "__main__":
    multiplication_table()

我把程式丟到 Thonny 去執行, 結果如下 :

>>> %Run multiplication_table.py  
1×1= 1  
1×2= 2  2×2= 4  
1×3= 3  2×3= 6  3×3= 9  
1×4= 4  2×4= 8  3×4=12  4×4=16  
1×5= 5  2×5=10  3×5=15  4×5=20  5×5=25  
1×6= 6  2×6=12  3×6=18  4×6=24  5×6=30  6×6=36  
1×7= 7  2×7=14  3×7=21  4×7=28  5×7=35  6×7=42  7×7=49  
1×8= 8  2×8=16  3×8=24  4×8=32  5×8=40  6×8=48  7×8=56  8×8=64  
1×9= 9  2×9=18  3×9=27  4×9=36  5×9=45  6×9=54  7×9=63  8×9=72  9×9=81  


看起來還不錯, 接下來改用難一點的, 叫它幫我寫一個用 pandas-ta 套件計算 DMI 指標並繪製 K 線圖的程式, 提示詞如下 :

"請用 pandas-ta 套件的函式 ta.adx() 寫一個計算趨向指標 DMI (Directional Movement Index), 並使用 kbar 套件繪製 K 線圖 (顯示成交量) 的 Python 程式"

生成的程式碼看起來有點複雜 : 

# dmi_with_pandas_ta_and_kbar.py
from __future__ import annotations
import sys
import warnings

import pandas as pd
import pandas_ta as ta
import yfinance as yf

# plotting imports will be attempted lazily
import matplotlib.pyplot as plt

warnings.filterwarnings("ignore", category=FutureWarning)

def download_ohlcv(ticker: str = "AAPL", period: str = "6mo", interval: str = "1d") -> pd.DataFrame:
    """
    從 yfinance 下載 OHLCV 資料並回傳 DataFrame,index 為 DatetimeIndex。
    """
    df = yf.download(ticker, period=period, interval=interval, progress=False)
    if df.empty:
        raise RuntimeError(f"無法從 yfinance 取得資料: {ticker} {period} {interval}")
    # 確保欄位小寫且名稱為 open, high, low, close, volume
    df = df.rename(columns={c: c.lower() for c in df.columns})
    # 有些 yfinance 回傳 'adj close',確保 'close' 存在
    if "close" not in df.columns and "adj close" in df.columns:
        df["close"] = df["adj close"]
    return df[["open", "high", "low", "close", "volume"]]

def compute_adx_dmi(df: pd.DataFrame, length: int = 14) -> pd.DataFrame:
    """
    使用 pandas-ta 的 DataFrame accessor .ta.adx() 計算 ADX 與 DMP/DMN(+DI, -DI)。
    會 append 欄位到原始 DataFrame:
      - ADX_{length}
      - DMP_{length}  (通常視為 +DI)
      - DMN_{length}  (通常視為 -DI)
    另外建立便於使用的標準欄位: '+DI'、'-DI'、'ADX'
    """
    # pandas_ta 的 DataFrame accessor
    df = df.copy()
    df.ta.adx(length=length, append=True)  # 會生成 ADX_{len}, DMP_{len}, DMN_{len}
    adx_col = f"ADX_{length}"
    dmp_col = f"DMP_{length}"
    dmn_col = f"DMN_{length}"
    if adx_col not in df.columns or dmp_col not in df.columns or dmn_col not in df.columns:
        raise RuntimeError("pandas-ta 未生成預期欄位,請確認 pandas-ta 版本與使用方法。")
    # 方便使用的別名
    df["+DI"] = df[dmp_col]
    df["-DI"] = df[dmn_col]
    df["ADX"] = df[adx_col]
    return df

def plot_with_kbar_if_available(df: pd.DataFrame, ticker: str = "AAPL") -> None:
    """
    嘗試使用 kbar 繪圖;若失敗則退回到 mplfinance。
    kbar API 在不同版本可能有差異,所以這邊採用保守的嘗試方式:
    - 嘗試呼叫常見的 kbar.plot 或 kbar.kline.plot 等函式
    - 若都不存在或呼叫失敗,使用 mplfinance 繪圖
    """
    try:
        import kbar
    except Exception:
        kbar = None

    # 準備要在副圖上畫的線
    add_indicator_lines = {
        "+DI": df["+DI"],
        "-DI": df["-DI"],
        "ADX": df["ADX"],
    }

    # 嘗試 kbar 的常見畫法(多數 kbar 版本會接受 DataFrame)
    if kbar is not None:
        try:
            # 嘗試幾種常見的 kbar API 風格
            # 1) kbar.plot(df, ...) 或 kbar.kline.plot(...)
            if hasattr(kbar, "plot"):
                try:
                    # 這種呼叫是最簡短直觀的嘗試
                    kbar.plot(df, type="candle", volume=True, title=f"{ticker} - K 線 (kbar)")
                    # kbar.plot 不一定支援 overlay 指標,若需要可另外畫
                    # 畫 +DI, -DI, ADX 在新視窗
                    fig, ax = plt.subplots(1, 1, figsize=(10, 3))
                    ax.plot(df.index, df["+DI"], label="+DI", color="green")
                    ax.plot(df.index, df["-DI"], label="-DI", color="red")
                    ax.plot(df.index, df["ADX"], label="ADX", color="blue")
                    ax.set_title(f"{ticker} - +DI / -DI / ADX")
                    ax.legend()
                    plt.show()
                    return
                except Exception:
                    # 如果 kbar.plot 存在但執行失敗,嘗試其他 API
                    pass
            # 2) kbar.kline.plot
            if hasattr(kbar, "kline") and hasattr(kbar.kline, "plot"):
                kbar.kline.plot(df, volume=True, title=f"{ticker} - K 線 (kbar.kline)")
                fig, ax = plt.subplots(1, 1, figsize=(10, 3))
                ax.plot(df.index, df["+DI"], label="+DI", color="green")
                ax.plot(df.index, df["-DI"], label="-DI", color="red")
                ax.plot(df.index, df["ADX"], label="ADX", color="blue")
                ax.set_title(f"{ticker} - +DI / -DI / ADX")
                ax.legend()
                plt.show()
                return
            # 3) kbar.KLine 或其他類別介面
            if hasattr(kbar, "KLine"):
                try:
                    k = kbar.KLine(df)
                    if hasattr(k, "plot"):
                        k.plot(volume=True, title=f"{ticker} - K 線 (kbar.KLine)")
                        fig, ax = plt.subplots(1, 1, figsize=(10, 3))
                        ax.plot(df.index, df["+DI"], label="+DI", color="green")
                        ax.plot(df.index, df["-DI"], label="-DI", color="red")
                        ax.plot(df.index, df["ADX"], label="ADX", color="blue")
                        ax.set_title(f"{ticker} - +DI / -DI / ADX")
                        ax.legend()
                        plt.show()
                        return
                except Exception:
                    pass
            # 如果 kbar 存在但上述方法都失敗,拋出例外讓程式使用 mplfinance
            raise RuntimeError("kbar 已安裝但常見 API 呼叫失敗或不相容,改回使用 mplfinance。")
        except Exception as e:
            print("kbar 繪圖失敗,原因:", e, file=sys.stderr)
            print("將使用 mplfinance 作為備援繪圖。", file=sys.stderr)

    # --- mplfinance 備援繪圖 ---
    try:
        import mplfinance as mpf
    except Exception as e:
        raise RuntimeError("未安裝 kbar,也未安裝 mplfinance。請安裝至少一個繪圖套件 (pip install kbar mplfinance)。") from e

    # 準備 mplfinance 的 addplot
    ap = [
        mpf.make_addplot(df["+DI"], color="g", width=1.0, panel=1, ylabel="+DI / -DI"),
        mpf.make_addplot(df["-DI"], color="r", width=1.0, panel=1),
        mpf.make_addplot(df["ADX"], color="b", width=1.0, panel=2, ylabel="ADX"),
    ]
    # 長條圖顏色根據漲跌
    mc = mpf.make_marketcolors(up="tab:red", down="tab:green", volume="gray")
    s = mpf.make_mpf_style(marketcolors=mc)

    mpf.plot(
        df,
        type="candle",
        volume=True,
        style=s,
        addplot=ap,
        title=f"{ticker} - K 線 (mplfinance 備援)",
        figsize=(12, 8),
        datetime_format="%Y-%m-%d",
        tight_layout=True,
    )


def main():
    # 這裡可以改成從 CLI 參數讀入
    TICKER = "AAPL"
    PERIOD = "6mo"
    INTERVAL = "1d"
    ADX_LENGTH = 14

    print(f"下載 {TICKER} 的 OHLCV 資料 ({PERIOD}, {INTERVAL})...")
    df = download_ohlcv(TICKER, PERIOD, INTERVAL)
    print(f"資料筆數: {len(df)},樣本時間區間: {df.index[0]} ~ {df.index[-1]}")

    print(f"計算 ADX / DMI (length={ADX_LENGTH})...")
    df = compute_adx_dmi(df, length=ADX_LENGTH)

    # 顯示最後幾列結果以供檢視
    print(df[["open", "high", "low", "close", "volume", "+DI", "-DI", "ADX"]].tail(8))

    print("嘗試以 kbar 繪圖(若 kbar 不可用或 API 不相容,將退回到 mplfinance)...")
    plot_with_kbar_if_available(df, ticker=TICKER)


if __name__ == "__main__":
    main()

執行結果出現錯誤 : 

>>> %Run dmi_with_pandas_ta_and_kbar.py   
下載 AAPL 的 OHLCV 資料 (6mo, 1d)...
Traceback (most recent call last):
  File "D:\python\test\dmi_with_pandas_ta_and_kbar.py", line 177, in <module>
    main()
  File "D:\python\test\dmi_with_pandas_ta_and_kbar.py", line 163, in main
    df = download_ohlcv(TICKER, PERIOD, INTERVAL)
  File "D:\python\test\dmi_with_pandas_ta_and_kbar.py", line 23, in download_ohlcv
    df = df.rename(columns={c: c.lower() for c in df.columns})
  File "D:\python\test\dmi_with_pandas_ta_and_kbar.py", line 23, in <dictcomp>
    df = df.rename(columns={c: c.lower() for c in df.columns})
AttributeError: 'tuple' object has no attribute 'lower'

顯然它並不知道 yfinance 已經改版了, 新版的傳回值是多層 columns 結構, 必須先轉成傳統的單層 DataFrame, 我將此錯誤訊息回饋給它 :

"上面的程式執行出現如下錯誤, 請 debug 並修正"

但是仍然出現錯誤, 乾脆提供我之前寫的 AOBV 指標測試程式給它 :

"請參考下面 AOBV 指標的寫法 :
# pandas_ta_aobv.py
import yfinance as yf     
import pandas_ta as ta
from kbar import KBar  

df=yf.download('0050.TW', start='2024-11-06', end='2025-01-09', auto_adjust=False)
df.columns=df.columns.map(lambda x: x[0])
df.ta.aobv(close='Close', append=True)
kb=KBar(df)
kb.addplot(df['OBV'], panel=2, color='red', width=1.2, ylabel='OBV')
kb.addplot(df['AOBV_LR_2'], panel=3, color='red', width=1.2, ylabel='AOBV')
kb.addplot(df['AOBV_SR_2'], panel=3, color='blue', width=1.2)
kb.plot(volume=True, mav=(5, 10), title='K 線圖與 OBV, AOBV 指標')"

終於給出可以正確執行的程式碼 : 

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
dmi_with_pandas_ta_and_kbar.py

說明:
- 參考你提供的 AOBV 範例寫法,使用 kbar.KBar 來繪製 K 線(顯示成交量)並把 +DI / -DI / ADX 加到副圖。
- 使用 pandas-ta 的 .ta.adx() 計算 ADX / +DI / -DI。
- 若欄位為 MultiIndex(yfinance 在某些情況會回傳),會取每個 tuple 的第一個元素,如同你的 AOBV 範例。
- 若 kbar 不可用,會退回到 mplfinance 繪圖(與前一版行為一致)。

用法:
python dmi_with_pandas_ta_and_kbar.py --ticker AAPL --period 6mo --interval 1d --length 14
或用 start/end:
python dmi_with_pandas_ta_and_kbar.py --ticker 0050.TW --start 2024-11-06 --end 2025-01-09 --interval 1d --length 14
"""
from __future__ import annotations
import argparse
import sys
import warnings

import pandas as pd
import pandas_ta as ta
import yfinance as yf

warnings.filterwarnings("ignore", category=FutureWarning)


def download_ohlcv(ticker: str, period: str | None, start: str | None, end: str | None, interval: str) -> pd.DataFrame:
    """
    下載 OHLCV,並針對 MultiIndex 欄位做簡單 normalize(取 tuple[0])。
    最後確保至少有 Open/High/Low/Close/Volume (TitleCase 或 Close/close) 可用。
    """
    # yfinance: 若有 start/end 則通常不用 period
    if start or end:
        df = yf.download(ticker, start=start, end=end, interval=interval, auto_adjust=False, progress=False)
    else:
        df = yf.download(ticker, period=period or "6mo", interval=interval, auto_adjust=False, progress=False)

    if df.empty:
        raise RuntimeError(f"無法從 yfinance 取得資料: {ticker} (period={period}, start={start}, end={end}, interval={interval})")

    # 如果是 MultiIndex,像 (('Open',''),('High','')) 等情況,取 tuple 的第 0 個元素 (與你的 AOBV 範例相同)
    if isinstance(df.columns, pd.MultiIndex):
        df.columns = df.columns.map(lambda x: x[0])

    # 有時候欄位是小寫、有時是 Title Case,統一處理:
    cols = {c: str(c) for c in df.columns}

    # 若只有 'Adj Close' 而沒有 'Close',建立 Close 欄位(跟 AOBV 範例做法一致)
    if "Close" not in df.columns and "Adj Close" in df.columns:
        df["Close"] = df["Adj Close"]
    if "close" not in df.columns and "adj close" in [c.lower() for c in df.columns]:
        # handle lowercase adj close
        # find actual column name that lower() == 'adj close'
        for c in df.columns:
            if str(c).lower() == "adj close":
                df["close"] = df[c]
                break

    # 最後檢查至少能找到 High/Low/Close (或 high/low/close)
    found_high = any(c.lower() == "high" for c in df.columns)
    found_low = any(c.lower() == "low" for c in df.columns)
    found_close = any(c.lower() == "close" for c in df.columns)
    found_open = any(c.lower() == "open" for c in df.columns)
    found_volume = any(c.lower() == "volume" for c in df.columns)

    if not (found_high and found_low and found_close):
        raise RuntimeError(f"yfinance 回傳欄位缺少 high/low/close (available: {list(df.columns)})")

    # 為了方便 pandas-ta 與 kbar,建立常見 TitleCase 欄位 (Open/High/Low/Close/Volume) 如果尚未有
    def ensure_title(col_lower, title):
        if title not in df.columns:
            for c in df.columns:
                if str(c).lower() == col_lower:
                    df[title] = df[c]
                    return

    ensure_title("open", "Open")
    ensure_title("high", "High")
    ensure_title("low", "Low")
    ensure_title("close", "Close")
    ensure_title("volume", "Volume")

    return df


def compute_adx_dmi(df: pd.DataFrame, length: int = 14) -> pd.DataFrame:
    """
    使用 pandas-ta 計算 ADX/DMP/DMN,並增加 '+DI','-DI','ADX' 欄位 (以 length 為 suffix)。
    需要欄位 High/Low/Close(不論大小寫)。
    """
    # pandas-ta 在 DataFrame accessor 上運作,接受 high/low/close 欄位名稱參數
    # 我們先嘗試用 TitleCase 欄位 (High/Low/Close)(符合 yfinance 常態)
    df2 = df.copy()

    # 指定欄位名稱(若沒有 TitleCase,改用小寫)
    high_col = "High" if "High" in df2.columns else next((c for c in df2.columns if c.lower() == "high"), None)
    low_col = "Low" if "Low" in df2.columns else next((c for c in df2.columns if c.lower() == "low"), None)
    close_col = "Close" if "Close" in df2.columns else next((c for c in df2.columns if c.lower() == "close"), None)

    if not (high_col and low_col and close_col):
        raise RuntimeError("需要 high/low/close 欄位才能計算 ADX/DMI")

    # pandas-ta 的 ta.adx 可以接受參數 high/low/close
    df2.ta.adx(length=length, high=high_col, low=low_col, close=close_col, append=True)

    # pandas-ta 會建立 ADX_{length}, DMP_{length}, DMN_{length}
    adx_col = f"ADX_{length}"
    dmp_col = f"DMP_{length}"
    dmn_col = f"DMN_{length}"

    if adx_col not in df2.columns or dmp_col not in df2.columns or dmn_col not in df2.columns:
        raise RuntimeError("pandas-ta 未生成預期欄位,請確認 pandas-ta 版本 (生成 ADX_{len}, DMP_{len}, DMN_{len})")

    # 建立更方便的欄位名稱(與你先前程式一致)
    df2["+DI"] = df2[dmp_col]
    df2["-DI"] = df2[dmn_col]
    df2["ADX"] = df2[adx_col]

    return df2


def plot_with_kbar(df: pd.DataFrame, ticker: str = "AAPL", mav: tuple | None = (5, 10)):
    """
    依照 AOBV 的寫法使用 kbar.KBar:
      - kb = KBar(df)
      - kb.addplot(series, panel=..., color=..., width=..., ylabel=...)
      - kb.plot(volume=True, mav=(...), title=...)
    若 kbar 不可用,退回到 mplfinance。
    """
    try:
        from kbar import KBar
    except Exception as e:
        KBar = None
        print("kbar 無法匯入,將使用 mplfinance 作為備援繪圖。", file=sys.stderr)

    # kbar 範例通常以 TitleCase 欄位 (Open/High/Low/Close/Volume)
    # 確保 DataFrame 中有 TitleCase 欄位,否則建立對應欄位
    df_plot = df.copy()
    for lower, title in (("open", "Open"), ("high", "High"), ("low", "Low"), ("close", "Close"), ("volume", "Volume")):
        if title not in df_plot.columns:
            for c in df_plot.columns:
                if str(c).lower() == lower:
                    df_plot[title] = df_plot[c]
                    break

    if KBar is not None:
        try:
            kb = KBar(df_plot)
            # +DI / -DI 放 panel=2,ADX 放 panel=3(參照你給的 AOBV 範例 panel 用法)
            kb.addplot(df_plot["+DI"], panel=2, color="green", width=1.2, ylabel="+DI")
            kb.addplot(df_plot["-DI"], panel=2, color="red", width=1.2)
            kb.addplot(df_plot["ADX"], panel=3, color="blue", width=1.2, ylabel="ADX")
            kb.plot(volume=True, mav=mav, title=f"{ticker} - K 線圖與 DMI (+DI/-DI/ADX)")
            return
        except Exception as e:
            print("使用 kbar 繪圖時發生錯誤,將退回到 mplfinance。錯誤:", e, file=sys.stderr)

    # mplfinance 備援繪圖
    try:
        import mplfinance as mpf
    except Exception as e:
        raise RuntimeError("未安裝 kbar,也未安裝 mplfinance。請安裝至少一個繪圖套件 (pip install kbar mplfinance)。") from e

    # mplfinance 需要 TitleCase 欄位
    df_mpf = df_plot.copy()

    ap = [
        mpf.make_addplot(df_mpf["+DI"], color="g", width=1.0, panel=1, ylabel="+DI / -DI"),
        mpf.make_addplot(df_mpf["-DI"], color="r", width=1.0, panel=1),
        mpf.make_addplot(df_mpf["ADX"], color="b", width=1.0, panel=2, ylabel="ADX"),
    ]
    mc = mpf.make_marketcolors(up="tab:red", down="tab:green", volume="gray")
    s = mpf.make_mpf_style(marketcolors=mc)

    mpf.plot(
        df_mpf.rename(columns={c: (c if isinstance(c, str) else str(c)) for c in df_mpf.columns}),
        type="candle",
        volume=True,
        style=s,
        addplot=ap,
        title=f"{ticker} - K 線圖與 DMI (+DI/-DI/ADX) (mplfinance)",
        figsize=(12, 8),
        mav=mav,
        datetime_format="%Y-%m-%d",
        tight_layout=True,
    )


def parse_args():
    p = argparse.ArgumentParser(description="計算 DMI (ADX/+DI/-DI) 並繪製 K 線 (kbar 或 mplfinance)")
    p.add_argument("--ticker", "-t", default="AAPL", help="股票代號,例如 AAPL 或 0050.TW")
    p.add_argument("--period", "-p", default="6mo", help="若未指定 start/end,使用 yfinance period (ex: 6mo, 1y)")
    p.add_argument("--start", help="開始日期 (YYYY-MM-DD),若指定則使用 start/end 而非 period")
    p.add_argument("--end", help="結束日期 (YYYY-MM-DD)")
    p.add_argument("--interval", "-i", default="1d", help="yfinance interval,例如 1d, 1h")
    p.add_argument("--length", "-l", type=int, default=14, help="ADX 計算長度 (預設 14)")
    p.add_argument("--mav", nargs="*", type=int, default=(5, 10), help="移動平均線 mav,例如 --mav 5 10")
    return p.parse_args()


def main():
    args = parse_args()
    try:
        print(f"下載 {args.ticker} 的 OHLCV 資料 (period={args.period}, start={args.start}, end={args.end}, interval={args.interval})...")
        df = download_ohlcv(args.ticker, args.period, args.start, args.end, args.interval)
        print(f"資料筆數: {len(df)},時間區間: {df.index[0]} ~ {df.index[-1]}")
        print(f"計算 ADX / DMI (length={args.length})...")
        df2 = compute_adx_dmi(df, length=args.length)
        # 顯示最後幾列確認
        display_cols = [c for c in ["Open", "High", "Low", "Close", "Volume", "+DI", "-DI", "ADX"] if c in df2.columns]
        print(df2[display_cols].tail(8))
        print("繪圖(優先使用 kbar,如不可用則使用 mplfinance)...")
        plot_with_kbar(df2, ticker=args.ticker, mav=tuple(args.mav) if args.mav else None)
    except Exception as e:
        print("執行發生錯誤:", e, file=sys.stderr)
        raise

if __name__ == "__main__":
    main()

執行結果 :




唉, 明明 11 行程式碼就能搞定, 為何要寫得這麼複雜呢? 


2025年10月3日 星期五

Hohaw 的 Python 量化投資 + AI 課程

由於 9 月份狂上 Hahow 企業版課程, 今天收到表示勤奮好學的 Lv.2 獎章 : 




學習歷程顯示自 2023 年 9 月加入以來已上完 162 門課, 目前完課率 84%, 還有 31 門已註冊但尚未上的課程, 年底前應該能達成 100% 完課率 : 




這些課程中最吸引我的是量化投資 + AI 課程 : 
  • 用 Python 打造自己的 AI 股票理專(一)
  • 用 Python 打造自己的 AI 股票理專(二)
  • 用 Python 打造自己的 AI 股票理專(三)
  • 用 Python 打造自己的 AI 股票理專(四)
  • 用 Python 打造小資族選股策略(一)預測你的財務人生曲線
  • 用 Python 打造小資族選股策略(二)打造客製化看盤軟體與選股策略
  • 用 Python 打造小資族選股策略(三)Python 理財資優生
  • 用 Python 理財:打造加密貨幣實戰策略(一)
  • 用 Python 理財:打造加密貨幣實戰策略(二)
  • 用 Python 理財:打造加密貨幣實戰策略(三)
  • 用 Python 理財:打造加密貨幣實戰策略(四)
  • 全方位 LLM 實戰攻略:Code 出你的 AI 智能助理
其中小資族選股策略系列我在個人帳號購買不久後企業版居然就上架了 (白買了呵呵). 我的企業版帳號使用期限似乎展延了, 最近有空要來重看一遍 (之前都囫圇吞棗). 

2025年4月15日 星期二

好站 : 最佳 GitHub Copilot 設定

上週在 GitHub Copilot 課程中得知保哥 Will 在 GitHub 上的詳細教學 :


雖然似乎目前也沒用到, 但我瀏覽器開太多頁啦! 關閉前還是先記一下得好, 或許明天就要用也說不定 (就像 Midjourney 收費後我就沒再用了, 但最近看書之後突然想付費來鑽研一下). 

2024年9月18日 星期三

註冊 Claude AI 帳戶與 API keys

今天在旗標的書 "ChatGPT 4 Turbo 萬用手冊" 上看到 Anthropic 的 Claude 模型的介紹, 它有提供 Python API 可用程式來測試聊天功能, 立馬上網來註冊帳號並申請 API key :


按 "Start building" 開始註冊 : 



可以用 Google 帳戶或 Email 註冊, 我都用 Email :




到信箱讀取驗證信, 按 Sign in to Anthropic Console 鈕 : 





填寫個資按 Create account 建立帳戶 : 




按 Get API key 建立金鑰 : 




按 Create key 鈕 : 




為此金鑰取個名字, 組織選擇 Default 後按 Create key :




按 Copy 鈕先將金鑰複製到記事本存檔備用 : 




可以建立多個不同名稱的金鑰 : 




如果之後要管理 API 金鑰可隨時登入 Claude Console 新增或刪除 :


Claude Console 的登入方式與一般服務不同的是你不需要紀錄密碼, 而是每次登入時先輸入 Email, 然後去信箱收取臨時密碼的方式登入. 

2024年9月12日 星期四

Google Gemini API 學習筆記 (一) : 文字聊天測試

八月下旬上徐爸的 ESP32-CAM 課程時因要用到 AI 大語言模型申請了 Google Gemini API 金鑰, 並使用 Postman 以 HTTP POST 請求測試聊天功能, 作法參考這篇 :  

前幾天忙完 Groq 上的 llama3 模型測試, 今天開始要來測試與 Groq 同樣是免費的 Google Gemini API, 官方有提供完整的教學文章 : 


網路上也有許多大神提供指引, 參考 :



1. 安裝 google-generativeai 套件 : 

直接用 pip install 指令安裝 : 

D:\python\test>pip install google-generativeai    
Collecting google-generativeai
  Downloading google_generativeai-0.8.0-py3-none-any.whl.metadata (3.9 kB)
Collecting google-ai-generativelanguage==0.6.9 (from google-generativeai)
  Downloading google_ai_generativelanguage-0.6.9-py3-none-any.whl.metadata (5.6 kB)
Collecting google-api-core (from google-generativeai)
  Downloading google_api_core-2.19.2-py3-none-any.whl.metadata (2.7 kB)
Collecting google-api-python-client (from google-generativeai)
  Downloading google_api_python_client-2.145.0-py2.py3-none-any.whl.metadata (6.7 kB)
Requirement already satisfied: google-auth>=2.15.0 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from google-generativeai) (2.22.0)
Requirement already satisfied: protobuf in c:\users\tony1\appdata\roaming\python\python310\site-packages (from google-generativeai) (4.24.2)
Requirement already satisfied: pydantic in c:\users\tony1\appdata\roaming\python\python310\site-packages (from google-generativeai) (2.5.3)
Requirement already satisfied: tqdm in c:\users\tony1\appdata\roaming\python\python310\site-packages (from google-generativeai) (4.66.1)
Requirement already satisfied: typing-extensions in c:\users\tony1\appdata\roaming\python\python310\site-packages (from google-generativeai) (4.9.0)
Collecting proto-plus<2.0.0dev,>=1.22.3 (from google-ai-generativelanguage==0.6.9->google-generativeai)
  Downloading proto_plus-1.24.0-py3-none-any.whl.metadata (2.2 kB)
Collecting googleapis-common-protos<2.0.dev0,>=1.56.2 (from google-api-core->google-generativeai)
  Downloading googleapis_common_protos-1.65.0-py2.py3-none-any.whl.metadata (1.5 kB)
Requirement already satisfied: requests<3.0.0.dev0,>=2.18.0 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from google-api-core->google-generativeai) (2.31.0)
Requirement already satisfied: cachetools<6.0,>=2.0.0 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from google-auth>=2.15.0->google-generativeai) (5.3.1)
Requirement already satisfied: pyasn1-modules>=0.2.1 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from google-auth>=2.15.0->google-generativeai) (0.3.0)
Requirement already satisfied: rsa<5,>=3.1.4 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from google-auth>=2.15.0->google-generativeai) (4.9)
Requirement already satisfied: six>=1.9.0 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from google-auth>=2.15.0->google-generativeai) (1.16.0)
Requirement already satisfied: urllib3<2.0 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from google-auth>=2.15.0->google-generativeai) (1.26.19)
Requirement already satisfied: httplib2<1.dev0,>=0.19.0 in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from google-api-python-client->google-generativeai) (0.22.0)
Collecting google-auth-httplib2<1.0.0,>=0.2.0 (from google-api-python-client->google-generativeai)
  Downloading google_auth_httplib2-0.2.0-py2.py3-none-any.whl.metadata (2.2 kB)
Collecting uritemplate<5,>=3.0.1 (from google-api-python-client->google-generativeai)
  Downloading uritemplate-4.1.1-py2.py3-none-any.whl.metadata (2.9 kB)
Requirement already satisfied: annotated-types>=0.4.0 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from pydantic->google-generativeai) (0.5.0)
Requirement already satisfied: pydantic-core==2.14.6 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from pydantic->google-generativeai) (2.14.6)
Requirement already satisfied: colorama in c:\users\tony1\appdata\local\programs\thonny\lib\site-packages (from tqdm->google-generativeai) (0.4.6)
Requirement already satisfied: grpcio<2.0dev,>=1.33.2 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from google-api-core[grpc]!=2.0.*,!=2.1.*,!=2.10.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,<3.0.0dev,>=1.34.1->google-ai-generativelanguage==0.6.9->google-generativeai) (1.57.0)
Collecting grpcio-status<2.0.dev0,>=1.33.2 (from google-api-core[grpc]!=2.0.*,!=2.1.*,!=2.10.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,<3.0.0dev,>=1.34.1->google-ai-generativelanguage==0.6.9->google-generativeai)
  Downloading grpcio_status-1.66.1-py3-none-any.whl.metadata (1.1 kB)
Requirement already satisfied: pyparsing!=3.0.0,!=3.0.1,!=3.0.2,!=3.0.3,<4,>=2.4.2 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from httplib2<1.dev0,>=0.19.0->google-api-python-client->google-generativeai) (3.0.9)
Requirement already satisfied: pyasn1<0.6.0,>=0.4.6 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from pyasn1-modules>=0.2.1->google-auth>=2.15.0->google-generativeai) (0.5.0)
Requirement already satisfied: charset-normalizer<4,>=2 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from requests<3.0.0.dev0,>=2.18.0->google-api-core->google-generativeai) (3.2.0)
Requirement already satisfied: idna<4,>=2.5 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from requests<3.0.0.dev0,>=2.18.0->google-api-core->google-generativeai) (3.4)
Requirement already satisfied: certifi>=2017.4.17 in c:\users\tony1\appdata\roaming\python\python310\site-packages (from requests<3.0.0.dev0,>=2.18.0->google-api-core->google-generativeai) (2023.7.22)
Collecting protobuf (from google-generativeai)
  Downloading protobuf-5.28.1-cp310-abi3-win_amd64.whl.metadata (592 bytes)
Collecting grpcio<2.0dev,>=1.33.2 (from google-api-core[grpc]!=2.0.*,!=2.1.*,!=2.10.*,!=2.2.*,!=2.3.*,!=2.4.*,!=2.5.*,!=2.6.*,!=2.7.*,!=2.8.*,!=2.9.*,<3.0.0dev,>=1.34.1->google-ai-generativelanguage==0.6.9->google-generativeai)
  Downloading grpcio-1.66.1-cp310-cp310-win_amd64.whl.metadata (4.0 kB)
Downloading google_generativeai-0.8.0-py3-none-any.whl (163 kB)
Downloading google_ai_generativelanguage-0.6.9-py3-none-any.whl (725 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 725.4/725.4 kB 3.7 MB/s eta 0:00:00
Downloading google_api_core-2.19.2-py3-none-any.whl (139 kB)
Downloading google_api_python_client-2.145.0-py2.py3-none-any.whl (12.2 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 12.2/12.2 MB 6.3 MB/s eta 0:00:00
Downloading google_auth_httplib2-0.2.0-py2.py3-none-any.whl (9.3 kB)
Downloading googleapis_common_protos-1.65.0-py2.py3-none-any.whl (220 kB)
Downloading proto_plus-1.24.0-py3-none-any.whl (50 kB)
Downloading uritemplate-4.1.1-py2.py3-none-any.whl (10 kB)
Downloading grpcio_status-1.66.1-py3-none-any.whl (14 kB)
Downloading protobuf-5.28.1-cp310-abi3-win_amd64.whl (431 kB)
Downloading grpcio-1.66.1-cp310-cp310-win_amd64.whl (4.3 MB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.3/4.3 MB 6.0 MB/s eta 0:00:00
Installing collected packages: uritemplate, protobuf, grpcio, proto-plus, googleapis-common-protos, grpcio-status, google-auth-httplib2, google-api-core, google-api-python-client, google-ai-generativelanguage, google-generativeai
  Attempting uninstall: protobuf
    Found existing installation: protobuf 4.24.2
    Uninstalling protobuf-4.24.2:
      Successfully uninstalled protobuf-4.24.2
  Attempting uninstall: grpcio
    Found existing installation: grpcio 1.57.0
    Uninstalling grpcio-1.57.0:
      Successfully uninstalled grpcio-1.57.0
ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
streamlit 1.27.0 requires protobuf<5,>=3.20, but you have protobuf 5.28.1 which is incompatible.
tensorflow-intel 2.13.0 requires protobuf!=4.21.0,!=4.21.1,!=4.21.2,!=4.21.3,!=4.21.4,!=4.21.5,<5.0.0dev,>=3.20.3, but you have protobuf 5.28.1 which is incompatible.
tensorflow-intel 2.13.0 requires typing-extensions<4.6.0,>=3.6.6, but you have typing-extensions 4.9.0 which is incompatible.
Successfully installed google-ai-generativelanguage-0.6.9 google-api-core-2.19.2 google-api-python-client-2.145.0 google-auth-httplib2-0.2.0 google-generativeai-0.8.0 googleapis-common-protos-1.65.0 grpcio-1.66.1 grpcio-status-1.66.1 proto-plus-1.24.0 protobuf-5.28.1 uritemplate-4.1.1

出現 protobuf 與 tensorflow-intel 這兩個相依套件的版本相容性問題, 但不影響使用 Gemini API.

檢視版本 :

>>> import google.generativeai as genai   
>>> genai.__version__     
'0.8.0'   

檢視套件內容 : 

>>> dir(genai)   
['ChatSession', 'GenerationConfig', 'GenerativeModel', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '__version__', 'annotations', 'caching', 'configure', 'create_tuned_model', 'delete_file', 'delete_tuned_model', 'embed_content', 'embed_content_async', 'get_base_model', 'get_file', 'get_model', 'get_operation', 'get_tuned_model', 'list_files', 'list_models', 'list_operations', 'list_tuned_models', 'protos', 'responder', 'string_utils', 'types', 'update_tuned_model', 'upload_file', 'utils']

使用自訂模組 members 檢視套件內容 : 

# members.py
import inspect 
def varname(x): 
    return [k for k,v in inspect.currentframe().f_back.f_locals.items() if v is x][0]
def list_members(parent_obj):
    members=dir(parent_obj)
    parent_obj_name=varname(parent_obj)       
    for mbr in members:
        child_obj=eval(parent_obj_name + '.' + mbr) 
        if not mbr.startswith('_'):
            print(mbr, type(child_obj))

參考 : 


>>> from members import list_members   
>>> list_members(genai)    
ChatSession <class 'type'>
GenerationConfig <class 'type'>
GenerativeModel <class 'type'>
annotations <class '__future__._Feature'>
caching <class 'module'>
configure <class 'function'>
create_tuned_model <class 'function'>
delete_file <class 'function'>
delete_tuned_model <class 'function'>
embed_content <class 'function'>
embed_content_async <class 'function'>
get_base_model <class 'function'>
get_file <class 'function'>
get_model <class 'function'>
get_operation <class 'function'>
get_tuned_model <class 'function'>
list_files <class 'function'>
list_models <class 'function'>
list_operations <class 'function'>
list_tuned_models <class 'function'>
protos <class 'module'>
responder <class 'module'>
string_utils <class 'module'>
types <class 'module'>
update_tuned_model <class 'function'>
upload_file <class 'function'>
utils <class 'module'>

這裡面主要會用到的是 configure() 函式與 GenerativeModel 類別, configure() 用來傳入 API 金鑰進行認證; GenerativeModel 則是用來建立指定模型之 GenerativeModel 物件以便進行聊天. 


2. 使用 API key 認證 : 

在進行聊天之前必須先呼叫 configure() 函式並傳入 api_key 參數進行認證, 為了避免金鑰洩漏, 可以將金鑰存入目前工作目錄下的隱藏檔 .env 內並取一個名稱例如 GEMINI_API_KEY : 




然後使用內建的 os 模組與第三方模組 dotenv 從 .env 檔中讀取指定名稱的金鑰 :

>>> import os   
>>> from dotenv import load_dotenv    
>>> load_dotenv()      
True 
>>> api_key=os.environ.get('GEMINI_API_KEY')      
>>> api_key   
'API KEY 在此'

然後呼叫 configure() 函式並傳入此金鑰 : 

>>> genai.configure(api_key=api_key)   

如果沒出現錯誤訊息表示金鑰認證成功. 


3. 建立 GenerativeModel 物件 : 

接下來就可以呼叫 GenerativeModel 類別的建構式 GenerativeModel() 並傳入模型名稱來建立 GenerativeModel 物件, 目前常用的模型如下 :


 模型名稱 說明
 gemini-1.5-pro 中型多模態模型, 可處理視訊/音訊/圖片/程式/文本, 可免費使用
 gemini-1.5-flash 輕量快速多模態模型, 可處理視訊/音訊/圖片/程式/文本, 可免費使用


參考 :


Flash 模型支援多達 40 種語言, 主打輕量快速, 雖然不似 Pro 版強大, 但在摘要生成, 聊天, 圖像與影片生成等功能上並沒有打多少折扣, 而且上下文窗口擴大為 32K 字元, 可與 Gemini 進行更長的對話. 免費帳戶 Pro 版每分鐘只能呼叫 2 次, Flash 版則是每秒 1500 次, 參考 :


例如 :

>>> model=genai.GenerativeModel('gemini-1.5-flash')    
>>> type(model)   
<class 'google.generativeai.generative_models.GenerativeModel'>

用 dir() 檢視這個 GenerativeModel 物件內容 : 

>>> dir(model)     
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', '_async_client', '_client', '_generation_config', '_get_tools_lib', '_model_name', '_prepare_request', '_safety_settings', '_system_instruction', '_tool_config', '_tools', 'cached_content', 'count_tokens', 'count_tokens_async', 'from_cached_content', 'generate_content', 'generate_content_async', 'model_name', 'start_chat']

用上面的自訂模組 members 檢視 : 

>>> from members import list_members   
>>> list_members(model)      
cached_content <class 'NoneType'>
count_tokens <class 'method'>
count_tokens_async <class 'method'>
from_cached_content <class 'method'>
generate_content <class 'method'>
generate_content_async <class 'method'>
model_name <class 'str'>
start_chat <class 'method'>

此 GenerativeModel 物件主要用來聊天的 API 為 generate_content() 與 generate_content_async() 方法, 兩者差別為同步/非同步呼叫. 呼叫時傳入提示詞會傳回一個 GenerateContentResponse 物件, 例如 :

>>> reply=model.generate_content('請簡單介紹量子力學與古典力學差別.')  
>>> type(reply)   
<class 'google.generativeai.types.generation_types.GenerateContentResponse'>    

回應文本會放在此物件的 text 屬性中 : 

>>> print(reply.text)     
## 量子力學 vs. 古典力學:簡單介紹

**古典力學** 描述的是宏觀世界,也就是我們日常生活中所見的物體的運動。它遵循牛頓定律,認為物體的位置和速度可以被精確測量,並且可以預測它們未來的運動。

**量子力學** 則是描述微觀世界,例如原子、電子和光子等。它與古典力學的區別在於:

* **量子化:** 在量子力學中,能量、動量、角動量等物理量只能取特定的離散值,而不是連續的。例如,電子在原子中只能處於特定的能級,而不是可以任意地取值。
* **波粒二象性:** 量子力學認為光子和電子等粒子同時具有波和粒子的性質。
* **不確定性原理:** 海森堡不確定性原理指出,我們無法同時精確地測量粒子的位置和動量。

**簡單總結:**

* 古典力學:適用於宏觀世界,可精確預測物體的運動。
* 量子力學:適用於微觀世界,描述粒子具有波粒二象性和不確定性。

**舉例:**

* 古典力學可以描述足球的運動軌跡。
* 量子力學可以描述原子中電子的能量狀態。

**補充說明:**

* 古典力學是量子力學的一個特例,當物體尺度很大時,量子效應可以忽略,古典力學就可以很好地描述其運動。
* 量子力學是現代物理學的基礎,它解釋了許多古典力學無法解釋的現象,例如原子光譜、半導體等。

可見 Gemini 的回應還不錯. 

但是如果問它 "愛情是甚麼?" 竟然出現錯誤訊息 :

>>> reply=model.generate_content('請問愛情是甚麼?')   
>>> print(reply.text)      
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Users\tony1\AppData\Local\Programs\Thonny\lib\site-packages\google\generativeai\types\generation_types.py", line 436, in text
    raise ValueError(
ValueError: Invalid operation: The `response.text` quick accessor requires the response to contain a valid `Part`, but none were returned. Please check the `candidate.safety_ratings` to determine if the response was blocked.

原因是這個提示詞被 Gemini 的預設安全設定阻擋了, 使得回應訊息中沒有 Part 這個屬性所致, 這是因為 Gemini 對於回應訊息的安全與否非常慎重, Gemini 有一個安全性過濾器用來篩選不安全或具有危害的回應, 關於安全性設定參考官網說明 :


我們可以在呼叫 generate_content() 時傳入一個 ssfety_settings 參數字典來關閉它們, 參考 :


>>> safety_settings={
        'HATE': 'BLOCK_NONE',
        'HARASSMENT': 'BLOCK_NONE',
        'SEXUAL' : 'BLOCK_NONE',
        'DANGEROUS' : 'BLOCK_NONE'
    }   
>>> reply=model.generate_content('請問愛情是甚麼?', safety_settings=safety_settings)    

關閉預設的安全性設定後便能取得 Gemini 模型的回應了 : 

>>> print(reply.text)    
愛情是一個非常複雜的情感,很難用簡單的定義概括。它可以包含以下一些元素:

* **親密感:**  對彼此的深切了解和信任,願意分享內心世界和感受。
* **激情:**  強烈的吸引力和慾望,包括生理上的吸引和情感上的渴望。
* **承諾:**  對彼此的長期承诺,願意共同面對人生的挑戰。

但是,愛情不只是一個抽象的概念,它也是一種主觀體驗,每个人对爱意的理解和感受都不同。它可以是:

* **浪漫的愛:**  充滿了激情和浪漫的愛情,常伴隨著心動和悸動。
* **友愛的愛:**  建立在共同興趣、價值觀和互相支持的基础上的愛情,充滿着理解和尊重。
* **家庭的愛:**  親人之間的愛,包含著無私的奉獻和無條件的包容。

總之,愛情是一種多樣且複雜的情感,它可以帶來幸福、快樂、痛苦和成長。它沒有標準答案,每個人生命中的愛情故事都獨一無二。

以下是一些哲學家和作家對愛情的看法:

* **柏拉圖:**  認為愛情是通往真理和美善的道路。
* **莎士比亞:**  以其作品中的爱情故事而闻名,他将爱情描绘成一种强烈的激情和痛苦的折磨。
* **弗洛伊德:**  认为爱情是一种本能的驱动力,是性欲和亲密感的结合。

最終,愛情的定義需要由每個人自己去探索和感受。


2024-09-17 補充 :

我在之前為了簡化 Grok 聊天寫的 ask_llm.py 模組內添加了 ask_gemini_text() 方法 :

# ask_llm.py
# Please install the following packages in advance:
# !pip install groq
# !pip install google-generativeai

import base64
from groq import Client
import google.generativeai as genai
from openai import OpenAI

def ask_groq_text(prompt, api_key, model='llama3-70b-8192'):
    client=Client(api_key=api_key)
    chat_completion=client.chat.completions.create(
        messages=[
            {"role": "user",
             "content": prompt,
             }],
        model=model,   
        )
    return chat_completion.choices[0].message.content

def ask_groq_vision_file(prompt, file, api_key):
    client=Client(api_key=api_key)
    with open(file, 'rb') as image_file:  
        base64_image=base64.b64encode(image_file.read()).decode('utf-8')
    image_url=f'data:image/jpeg;base64,{base64_image}' 
    content=[{'type': 'text', 'text': prompt}, 
             {'type': 'image_url', 'image_url': {'url': image_url}}]
    chat_completion=client.chat.completions.create(
         messages=[{'role': 'user', 'content': content}],
         model='llava-v1.5-7b-4096-preview'
         )
    return chat_completion.choices[0].message.content

def ask_groq_vision_url(prompt, url, api_key):
    client=Client(api_key=api_key)
    content=[{'type': 'text', 'text': prompt},  
             {'type': 'image_url', 'image_url': {'url': url}}] 
    chat_completion=client.chat.completions.create(
         messages=[{'role': 'user', 'content': content}],
         model='llava-v1.5-7b-4096-preview'
         )
    return chat_completion.choices[0].message.content

def ask_gemini_text(prompt, api_key, model='gemini-1.5-flash'):
    genai.configure(api_key=api_key)
    model=genai.GenerativeModel(model)
    safety_settings={
        'HATE': 'BLOCK_NONE',
        'HARASSMENT': 'BLOCK_NONE',
        'SEXUAL' : 'BLOCK_NONE',
        'DANGEROUS' : 'BLOCK_NONE'
        }    
    reply=model.generate_content(prompt, safety_settings=safety_settings)
    return reply.text

首先從隱藏檔 .env 讀取 GROQ_API_KEY 變數 :

>>> import os      
>>> from dotenv import load_dotenv   
>>> load_dotenv()    
True
>>> gemini_api_key=os.environ.get('GEMINI_API_KEY')    

使用時先從 ask_llm 模組匯入所有函式 :

>>> from ask_llm import *   

設定提示詞後即可呼叫 ask_gemini() 聊天 : 

>>> prompt='甚麼是量子糾纏? 請以能讓小學生了解的程度簡述並以繁體中文 (zh_Hant) 回答'   
>>> print(ask_gemini_text(prompt, gemini_api_key))   
想像一下,你有兩個特別的彈珠,它們是雙胞胎,雖然分開了,卻有一種神奇的聯繫。

當你把其中一個彈珠塗成紅色,另一個彈珠瞬間就會變成藍色!而且,無論它們相隔多遠,這個神奇的顏色變化都會瞬間發生。

這就是量子糾纏,兩個粒子即使相隔很遠,也能互相影響,就像兩個心靈相通的雙胞胎一樣。

不過,量子糾纏的粒子不像彈珠一樣只有紅藍兩種顏色,它們可以有很多不同的狀態,例如,它們可以同時像波一樣擴散,也可以像粒子一樣集中,而當其中一個粒子的狀態改變時,另一個粒子也會瞬間跟著改變。

量子糾纏是一個非常奇妙的現象,科學家們還在研究它的更多秘密,希望未來可以利用它來開發更先進的科技,例如量子電腦。

-------------------------------------------------

如果要呼叫 Pro 模型 model 要傳入 'gemini-pro' (每分鐘只能呼叫 2 次) :  

>>> print(ask_gemini_text(prompt, gemini_api_key, 'gemini-pro'))  
量子糾纏就像兩個特別的朋友,即使相隔很遠,他們也能心有靈犀。

想像一下你有兩個盒子,每個盒子裡都有一個小球。如果你打開一個盒子,發現裡面的球是紅色的,那麼你不用打開另一個盒子,你也知道另一個盒子裡的小球一定是紅色的。這兩個球就像量子糾纏的朋友,它們之間有種特別的聯繫,即使它們分開很遠,也能「感應」到彼此。

這個現象很神奇,就像魔術一樣,但它真的是自然界裡真實存在的現象。科學家們已經做過很多實驗,證明了量子糾纏是真實的。

參考 :



2026-04-05 補充 :

Google 已於 2024 年底推出新版的 SDK 套件 google-genai 來取代 google-generativeai, 此舉主要的目的是要解決谷歌 AI 的 API 混亂問題, 因為在此之前, 其 API 分成個人開發版 (AI Studio) 和企業版 (Vertex AI), 兩邊的寫法完全不同. 新版的 google-genai 則統一了 API 的寫法, 它在 2025 年 5 月正式轉為 GA 版本 (穩定版), 全面取代舊有的 SDK, 上面的舊版套件 google-generativeai 已於 2025 年底被宣布停止維護 (End-of-Life). 

首先用 pip 安裝 : 

pip install google-genai 

也可以安裝新版的 langchain-google-genai 套件, 因為 google-genai 做為它的底層依賴也會被一併安裝. 用法如下 : 

先匯入 google.genai :

from google import genai 

建立 Client 物件 : 

client=genai.Client(api_key=api_key)

呼叫生成模型 : 

response=client.models.generate_content(
    model='gemini-2.5-flash', 
    contents='你是誰?'
    )

注意, 新版不再區分 Chat 或 Completion, 統一用 models.generate_content(). 回應內容放在 response.text 內, 例如 : 

(myvenv) D:\python\test>python   
Python 3.12.1 (tags/v3.12.1:2305ca5, Dec  7 2023, 22:03:25) [MSC v.1937 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from google import genai   
>>> from dotenv import dotenv_values   
>>> config=dotenv_values('.env')   
>>> gemini_api_key=config.get('GEMINI_API_KEY')   
>>> client=genai.Client(api_key=gemini_api_key)   
>>> response=client.models.generate_content(  
     model='gemini-2.5-flash', 
     contents='你是誰?'
     )
>>> response.text  
'我是一个大型语言模型,由 Google 训练。'

檢視 response :

>>> response   
GenerateContentResponse(
  automatic_function_calling_history=[],
  candidates=[
    Candidate(
      content=Content(
        parts=[
          Part(
            text='我是一个大型语言模型,由 Google 训练。'
          ),
        ],
        role='model'
      ),
      finish_reason=<FinishReason.STOP: 'STOP'>,
      index=0
    ),
  ],
  model_version='gemini-2.5-flash',
  response_id='xrPRae2kLKmf1e8P0NySkAY',
  sdk_http_response=HttpResponse(
    headers=<dict len=12>
  ),
  usage_metadata=GenerateContentResponseUsageMetadata(
    candidates_token_count=11,
    prompt_token_count=4,
    prompt_tokens_details=[
      ModalityTokenCount(
        modality=<MediaModality.TEXT: 'TEXT'>,
        token_count=4
      ),
    ],
    thoughts_token_count=26,
    total_token_count=41
  )
)

用 lanchain-google-genai 套件更方便, 參考 :


2024年9月10日 星期二

Groq Python API 圖片測試

除了文字聊天外, Groq 的 llava-v1.5-7b-4096-preview 模型也具有圖像識別與描述能力, 可以指定網路上的圖片網址要求模型描述此圖片; 也可以透過 base64 套件將本機圖片編碼後上傳並要求對圖片加以描述. 

本系列之前的文章參考 :


本篇測試參考官方教學文件 :



1. 指定圖片網址 :

我在網路上隨便找了一張車牌圖片 : 




先從隱藏檔 .env 讀出 Grok 的 API key :

>>> import os   
>>> from dotenv import load_dotenv   
>>> load_dotenv()   
True
>>> groq_api_key=os.environ.get('GROQ_API_KEY')    

然後從 groq 匯入 Client 類別, 呼叫其建構式 Client() 並傳入 api_key 參數 : 

>>> from groq import Client  
>>> client=Client(api_key=groq_api_key)   

使用 llava-v1.5-7b-4096-preview 模型向其詢問圖片內容是甚麼 (What's in this image?), 與文字聊天一樣是呼叫 client.chat.completions.create(), 但 messages 裡面的 content 屬性不是傳入字串, 而是一個字典串列, prompt 放在 type 為 'text' 的屬性中; 而圖片網址則是放在 type 為 'image_url' 的屬性中, 語法架構如下 : 

prompt='What is in this image?'
image_url='圖片網址'
content=[{'type': 'text', 'text': prompt}, 
               {'type': 'image_url', 'image_url': {'url': image_url}}]
chat_completion=client.chat.completions.create(
     messages=[{'role': 'user', 'content': content}],
     model='llava-v1.5-7b-4096-preview'
     )

例如 : 

>>> prompt='What is in this image?'    
>>> image_url='https://maxaiot.com/image/alpr/alprBgImage.png'      
>>> content=[{'type': 'text', 'text': prompt},  
               {'type': 'image_url', 'image_url': {'url': image_url}}]   
>>> content    
[{'type': 'text', 'text': 'What is in this image?'}, {'type': 'image_url', 'image_url': {'url': 'https://maxaiot.com/image/alpr/alprBgImage.png'}}]   
>>> chat_completion=client.chat.completions.create(
     messages=[{'role': 'user', 'content': content}],
     model='llava-v1.5-7b-4096-preview'
     )    

檢視回應內容, 有正確辨識出此為白色車牌, 但車牌號碼卻辨識錯誤為 BC-55  : 

>>> chat_completion.choices[0].message.content   
'The image features an old looking white license plate that starts with the initials BC. The plate number, BC-55, is displayed on the plate. It creates an atmosphere reminiscent of a vintage photo.'

由於隨機性, 再次執行會得到不同結果 : 

>>> chat_completion=client.chat.completions.create(
     messages=[{'role': 'user', 'content': content}],
     model='llava-v1.5-7b-4096-preview'
     ) 
>>> chat_completion.choices[0].message.content    
'The image features a closeup view of a license plate, with each letter and number contrasting to the background. The license plate comprises the letters "BBC" and the digits "C1 S-5657". The plate has a black-and-white appearance and frames the entire image.'

辨識結果也是不完全正確, 可見此模型效果目前不是很好. 改用另一張圖也是一樣 :





>>> image_url='https://img.ltn.com.tw/Upload/auto/page/2019/11/03/191103-13921-1-apGNK.png'   
>>> content=[{'type': 'text', 'text': prompt},  
               {'type': 'image_url', 'image_url': {'url': image_url}}]   
>>> chat_completion=client.chat.completions.create(
     messages=[{'role': 'user', 'content': content}],
     model='llava-v1.5-7b-4096-preview'
     )   
>>> chat_completion.choices[0].message.content         
'The image features a white license plate with bright blue stars and the numbers 338 underneath. The license plate is displayed sideways and is very big, covering a large portion of the image. It appears to be sitting on a small side on the bottom left corner as well. The rest of the plate is prominent, giving a clear view of the number plate.'

數字 3388 只辨識出 338, 字母 NBX 沒有辨識出來. 再次辨識結果不同 :

>>> chat_completion=client.chat.completions.create(
     messages=[{'role': 'user', 'content': content}],
     model='llava-v1.5-7b-4096-preview'
     )   
>>> chat_completion.choices[0].message.content   
'The image features a vehicle license plate, specifically an Illinois license plate from the state of Illinois. The license plate reads "NBOX-38338388." The license plate is round, placed below vents, and has a blue star in the upper-middle section, representing a city within the state. The license plate is attached to the front of a vehicle, indicating its registration in Illinois.'

這次字母辨識出 NBOX 多了 O, 數字 3388 辨識為 3833838, 是伊利諾州車牌? 


2. 上傳本機圖片 :

若要上傳本機圖片讓 Groq 的模型辨識, 需使用 base64 套件予以編碼, 先匯入套件 :

>>> import base64   

將上面那張 ABC-5678 車牌抓圖後另存為 ABC-5678.png 檔案於目前工作目錄下, 然後以 'rb' 模式開啟後傳給 base64.b64encode() 函式進行編碼後以 utf-8 格式解碼 :

>>> with open('ABC-5678.png', "rb") as image_file:  
  base64_image=base64.b64encode(image_file.read()).decode('utf-8')   

檢視圖片用 base64 編碼後的結果 : 

>>> base64_image      
'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…

看起來是非常常的亂碼. 

網路上也有將圖片轉成 base64 編碼的線上服務, 參考 :





結果與上面是一樣的. 呼叫聊天 API 時只要將 image_url 從網址改成如下使用 f 字串嵌入 base64 編碼的字串即可 : 

f'data:image/jpeg;base64,{base64_image}'    

語法架構如下 : 

prompt='What is in this image?'
image_url=f'data:image/jpeg;base64,{base64_image}
content=[{'type': 'text', 'text': prompt}, 
               {'type': 'image_url', 'image_url': {'url': image_url}}]
chat_completion=client.chat.completions.create(
     messages=[{'role': 'user', 'content': content}],
     model='llava-v1.5-7b-4096-preview'
     )

例如 :

>>> image_url=f'data:image/jpeg;base64,{base64_image}'     
>>> content=[{'type': 'text', 'text': prompt}, 
               {'type': 'image_url', 'image_url': {'url': image_url}}]    
>>> chat_completion=client.chat.completions.create(
     messages=[{'role': 'user', 'content': content}],
     model='llava-v1.5-7b-4096-preview'
     )   
>>> chat_completion.choices[0].message.content     
"The image features a vehicle's licence expired license plate and the numbers CD-536 from Quebec. This provides some information about the vehicle's ownership. The white license plate is a primary component of the photo. As a notable detail, the white numbers are slightly obscured or partially blacked out, giving the appearance of possibly expired or invalid."     

從以上測試可知, Grok 的 llava-v1.5-7b-4096-preview 模型對圖片中的文字辨認之正確性還有待加強. 最後來試試一般圖片的描述, 我在 Pixabay 上找到一張狗狗圖 :


Source : Pixabay


此圖片的網址是 :


先用英文提示詞 :

>>> image_url='https://cdn.pixabay.com/photo/2024/03/15/17/50/dogs-8635461_1280.jpg'   
>>> prompt='What is in this image?'  
>>> content=[{'type': 'text', 'text': prompt},  
               {'type': 'image_url', 'image_url': {'url': image_url}}]         
>>> chat_completion=client.chat.completions.create(
     messages=[{'role': 'user', 'content': content}],
     model='llava-v1.5-7b-4096-preview'
     )      
>>> chat_completion.choices[0].message.content    
'In this image, there are a group of young white dogs, possibly a litter, sleeping together on a bed. They are either laying or sleeping near each other, making it a heartwarming scene of puppies cuddling in close.'

結果還不錯, 改用中文問看看 : 

>>> prompt='這是一張甚麼樣的圖? 請用繁體中文(zh_Hant)回答'    
>>> content=[{'type': 'text', 'text': prompt},  
               {'type': 'image_url', 'image_url': {'url': image_url}}]         
>>> chat_completion=client.chat.completions.create(
     messages=[{'role': 'user', 'content': content}],
     model='llava-v1.5-7b-4096-preview'
     )      
>>> chat_completion.choices[0].message.content     
'這是一張多片白小狗在毛中睡覺和朋友們相靠垂。他們的相似和和親密的聚結一起產生一張吸引人心的照片。'

可見有些詞彙有點怪, 但基本上還可以. 

但是如果問圖中有幾隻狗, 跟辨識車牌號碼一樣不準, 先用英文問 : 

>>> image_url='https://cdn.pixabay.com/photo/2024/03/15/17/50/dogs-8635461_1280.jpg'       
>>> prompt='How many dogs are there in the picture?'       
>>> content=[{'type': 'text', 'text': prompt},  
               {'type': 'image_url', 'image_url': {'url': image_url}}]        
>>> chat_completion=client.chat.completions.create(
     messages=[{'role': 'user', 'content': content}],
     model='llava-v1.5-7b-4096-preview'
     )      
>>> chat_completion.choices[0].message.content     
'Since the image features a line of dogs sleeping together on the same bed, it is likely that there are several dogs present in the picture. However, without a specific count or visible number of dogs, it is not possible to provide an exact number of dogs.'

只說很多狗, 不講幾隻. 哈哈. 再問一次回答 6 隻 : 

>>> chat_completion=client.chat.completions.create(
     messages=[{'role': 'user', 'content': content}],
     model='llava-v1.5-7b-4096-preview'
     )    
>>> chat_completion.choices[0].message.content    
'There are six Poodle puppies in the picture.'

用中文問 :

>>> prompt='這張圖中有幾隻狗狗? 請用繁體中文(zh_Hant)回答'   
>>> content=[{'type': 'text', 'text': prompt},   
               {'type': 'image_url', 'image_url': {'url': image_url}}]    
>>> chat_completion=client.chat.completions.create(
     messages=[{'role': 'user', 'content': content}],
     model='llava-v1.5-7b-4096-preview'
     )     
>>> chat_completion.choices[0].message.content   
'在這張圖中有十幾隻小狗.'   

總之, 這個 preview 的視覺模型描述還可以, 但辨識還不行. 


3. 聊天函式 :

我把上面的兩種圖片聊天與之前的文字聊天程式碼包裝成 ask_llm 模組方便呼叫 : 

# ask_llm.py 
import base64
from groq import Client

def ask_groq_text(prompt, api_key, model='llama3-70b-8192'):
    client=Client(api_key=api_key)
    chat_completion=client.chat.completions.create(
        messages=[
            {"role": "user",
             "content": prompt,
             }],
        model=model,   
        )
    return chat_completion.choices[0].message.content

def ask_groq_vision_file(prompt, file, api_key):
    client=Client(api_key=api_key)
    with open(file, 'rb') as image_file:  
        base64_image=base64.b64encode(image_file.read()).decode('utf-8')
    image_url=f'data:image/jpeg;base64,{base64_image}' 
    content=[{'type': 'text', 'text': prompt}, 
             {'type': 'image_url', 'image_url': {'url': image_url}}]
    chat_completion=client.chat.completions.create(
         messages=[{'role': 'user', 'content': content}],
         model='llava-v1.5-7b-4096-preview'
         )
    return chat_completion.choices[0].message.content

def ask_groq_vision_url(prompt, url, api_key):
    client=Client(api_key=api_key)
    content=[{'type': 'text', 'text': prompt},  
             {'type': 'image_url', 'image_url': {'url': url}}] 
    chat_completion=client.chat.completions.create(
         messages=[{'role': 'user', 'content': content}],
         model='llava-v1.5-7b-4096-preview'
         )
    return chat_completion.choices[0].message.content

使用時先從 ask_llm 模組匯入所有函式, 只要傳入 prompt, url/file, 與 api_key 參數即可呼叫 ask_groq_vision_url() 或 ask_groq_vision_file() 進行圖片聊天 :

>>> from ask_llm import *    

首先測試 url 圖片聊天, 先從隱藏檔 .env 讀取 GROQ_API_KEY 變數 :

>>> import os      
>>> from dotenv import load_dotenv   
>>> load_dotenv()   
True
>>> groq_api_key=os.environ.get('GROQ_API_KEY')   

設定提示詞與圖片 url 變數 :

>>> prompt='What is in this image?'   
>>> url='https://maxaiot.com/image/alpr/alprBgImage.png'     

呼叫 ask_groq_vision_url() 函式 :

>>> ask_groq_vision_url(prompt, url, groq_api_key)    
"In this image, there is a black and white picture of a car tag or vehicle assignment on a license plate. The tag is accompanied by numbers, such as `55-BC' and `57-C<55' depending on which description refers to the specific numbers. The tag has a black background with white lettering and possibly includes information related to the vehicle assigned to the tag."

接下來是上傳本機圖片, 設定提示詞與圖片之路徑檔名 :

>>> prompt='What is in this image?'   
>>> file='ABC-5678.png'    

呼叫 ask_groq_vision_file() 函式 :

>>> ask_groq_vision_file(prompt, file, groq_api_key)
'The image is a license plate, showcasing the licence plate designated C-566.'